From 6a88c61d1253e6b7f3d4de9c54da789213c6c896 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 21 Oct 2022 14:49:29 +0200 Subject: [PATCH 001/679] Group voice broadcast controller buttons in a Flow --- ...e_event_voice_broadcast_listening_stub.xml | 21 +++++++++---------- ...e_event_voice_broadcast_recording_stub.xml | 21 ++++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 248c04a2f6..97f15967e1 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -118,6 +118,15 @@ app:barrierMargin="12dp" app:constraint_referenced_ids="roomAvatarImageView,titleText,broadcasterViewGroup,voiceBroadcastViewGroup" /> + + - - + android:indeterminateTint="?vctr_content_secondary" /> diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml index e3bb85138d..7b45a194e8 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml @@ -64,6 +64,15 @@ app:barrierMargin="12dp" app:constraint_referenced_ids="roomAvatarImageView,titleText" /> + + + android:src="@drawable/ic_recording_dot" /> + android:src="@drawable/ic_stop" /> From 1566adb66992894f2713a06a73f5340aca19ead2 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 14:10:51 +0200 Subject: [PATCH 002/679] Timeline - Add abstraction on voice broadcast items --- .../src/main/res/values/donottranslate.xml | 1 + ...stylable_voice_broadcast_metadata_view.xml | 9 ++ .../factory/VoiceBroadcastItemFactory.kt | 4 + .../item/AbsMessageVoiceBroadcastItem.kt | 96 ++++++++++++++++ .../MessageVoiceBroadcastListeningItem.kt | 104 +++--------------- .../MessageVoiceBroadcastRecordingItem.kt | 54 ++------- .../voicebroadcast/VoiceBroadcastPlayer.kt | 29 +++-- .../views/VoiceBroadcastMetadataView.kt | 66 +++++++++++ vector/src/main/res/drawable/ic_timer.xml | 9 ++ .../res/drawable/ic_voice_broadcast_16.xml | 21 ---- .../res/drawable/ic_voice_broadcast_mic.xml | 12 ++ ...e_event_voice_broadcast_listening_stub.xml | 77 +++++-------- ...e_event_voice_broadcast_recording_stub.xml | 36 +++++- .../layout/view_voice_broadcast_metadata.xml | 27 +++++ 14 files changed, 329 insertions(+), 216 deletions(-) create mode 100644 library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt create mode 100644 vector/src/main/res/drawable/ic_timer.xml delete mode 100644 vector/src/main/res/drawable/ic_voice_broadcast_16.xml create mode 100644 vector/src/main/res/drawable/ic_voice_broadcast_mic.xml create mode 100644 vector/src/main/res/layout/view_voice_broadcast_metadata.xml diff --git a/library/ui-strings/src/main/res/values/donottranslate.xml b/library/ui-strings/src/main/res/values/donottranslate.xml index 741d23dbc6..bfe751ef5a 100755 --- a/library/ui-strings/src/main/res/values/donottranslate.xml +++ b/library/ui-strings/src/main/res/values/donottranslate.xml @@ -2,6 +2,7 @@ + Not implemented yet in ${app_name} diff --git a/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml b/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml new file mode 100644 index 0000000000..1f72eeb396 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_voice_broadcast_metadata_view.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 5dc601a91a..7b8c927186 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -67,6 +67,7 @@ class VoiceBroadcastItemFactory @Inject constructor( createRecordingItem( params.event.roomId, eventsGroup.groupId, + mostRecentMessageContent.voiceBroadcastState, highlight, callback, attributes @@ -87,6 +88,7 @@ class VoiceBroadcastItemFactory @Inject constructor( private fun createRecordingItem( roomId: String, voiceBroadcastId: String, + voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, @@ -100,6 +102,8 @@ class VoiceBroadcastItemFactory @Inject constructor( .colorProvider(colorProvider) .drawableProvider(drawableProvider) .voiceBroadcastRecorder(voiceBroadcastRecorder) + .voiceBroadcastId(voiceBroadcastId) + .voiceBroadcastState(voiceBroadcastState) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(callback) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt new file mode 100644 index 0000000000..cbf35e89d2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import im.vector.app.R +import im.vector.app.core.extensions.tintBackground +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.DrawableProvider +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import org.matrix.android.sdk.api.util.MatrixItem + +abstract class AbsMessageVoiceBroadcastItem : AbsMessageItem() { + + @EpoxyAttribute + var callback: TimelineEventController.Callback? = null + + @EpoxyAttribute + lateinit var colorProvider: ColorProvider + + @EpoxyAttribute + lateinit var drawableProvider: DrawableProvider + + @EpoxyAttribute + lateinit var voiceBroadcastId: String + + @EpoxyAttribute + var voiceBroadcastState: VoiceBroadcastState? = null + + @EpoxyAttribute + var roomItem: MatrixItem? = null + + override fun isCacheable(): Boolean = false + + override fun bind(holder: H) { + super.bind(holder) + renderHeader(holder) + } + + private fun renderHeader(holder: H) { + with(holder) { + roomItem?.let { + attributes.avatarRenderer.render(it, roomAvatarImageView) + titleText.text = it.displayName + } + } + renderLiveIcon(holder) + renderMetadata(holder) + } + + private fun renderLiveIcon(holder: H) { + with(holder) { + when (voiceBroadcastState) { + VoiceBroadcastState.STARTED, + VoiceBroadcastState.RESUMED -> { + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) + liveIndicator.isVisible = true + } + VoiceBroadcastState.PAUSED -> { + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) + liveIndicator.isVisible = true + } + VoiceBroadcastState.STOPPED, null -> { + liveIndicator.isVisible = false + } + } + } + } + + abstract fun renderMetadata(holder: H) + + abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { + val liveIndicator by bind(R.id.liveIndicator) + val roomAvatarImageView by bind(R.id.roomAvatarImageView) + val titleText by bind(R.id.titleText) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 5b58dda4e6..135053d9a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -18,56 +18,26 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick -import im.vector.app.core.extensions.tintBackground -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.home.room.detail.RoomDetailAction -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import org.matrix.android.sdk.api.util.MatrixItem +import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass -abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem() { - - @EpoxyAttribute - var callback: TimelineEventController.Callback? = null +abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem() { @EpoxyAttribute var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null - @EpoxyAttribute - lateinit var voiceBroadcastId: String - - @EpoxyAttribute - var voiceBroadcastState: VoiceBroadcastState? = null - @EpoxyAttribute var broadcasterName: String? = null - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - lateinit var drawableProvider: DrawableProvider - - @EpoxyAttribute - var roomItem: MatrixItem? = null - - @EpoxyAttribute - var title: String? = null - private lateinit var playerListener: VoiceBroadcastPlayer.Listener - override fun isCacheable(): Boolean = false - override fun bind(holder: Holder) { super.bind(holder) bindVoiceBroadcastItem(holder) @@ -75,51 +45,20 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem - renderState(holder, state) + renderPlayingState(holder, state) } - voiceBroadcastPlayer?.addListener(playerListener) - renderHeader(holder) - renderLiveIcon(holder) + voiceBroadcastPlayer?.addListener(voiceBroadcastId, playerListener) } - private fun renderHeader(holder: Holder) { + override fun renderMetadata(holder: Holder) { with(holder) { - roomItem?.let { - attributes.avatarRenderer.render(it, roomAvatarImageView) - titleText.text = it.displayName - } - broadcasterNameText.text = broadcasterName + broadcasterNameMetadata.value = broadcasterName.orEmpty() + voiceBroadcastMetadata.isVisible = true + listenersCountMetadata.isVisible = false } } - private fun renderLiveIcon(holder: Holder) { - with(holder) { - when (voiceBroadcastState) { - VoiceBroadcastState.STARTED, - VoiceBroadcastState.RESUMED -> { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.PAUSED -> { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.STOPPED, null -> { - liveIndicator.isVisible = false - } - } - } - } - - private fun renderState(holder: Holder, state: VoiceBroadcastPlayer.State) { - if (isCurrentMediaActive()) { - renderActiveMedia(holder, state) - } else { - renderInactiveMedia(holder) - } - } - - private fun renderActiveMedia(holder: Holder, state: VoiceBroadcastPlayer.State) { + private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) { with(holder) { bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING @@ -143,34 +82,19 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem(R.id.liveIndicator) - val roomAvatarImageView by bind(R.id.roomAvatarImageView) - val titleText by bind(R.id.titleText) + class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) { val playPauseButton by bind(R.id.playPauseButton) val bufferingView by bind(R.id.bufferingView) - val broadcasterNameText by bind(R.id.broadcasterNameText) + val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata) + val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata) + val listenersCountMetadata by bind(R.id.listenersCountMetadata) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index c417053b2a..b766698851 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -17,46 +17,23 @@ package im.vector.app.features.home.room.detail.timeline.item import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick -import im.vector.app.core.extensions.tintBackground -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder -import org.matrix.android.sdk.api.util.MatrixItem +import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass -abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem() { - - @EpoxyAttribute - var callback: TimelineEventController.Callback? = null +abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem() { @EpoxyAttribute var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - lateinit var drawableProvider: DrawableProvider - - @EpoxyAttribute - var roomItem: MatrixItem? = null - - @EpoxyAttribute - var title: String? = null - private lateinit var recorderListener: VoiceBroadcastRecorder.Listener - override fun isCacheable(): Boolean = false - override fun bind(holder: Holder) { super.bind(holder) bindVoiceBroadcastItem(holder) @@ -65,32 +42,26 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem { stopRecordButton.isEnabled = true recordButton.isEnabled = true - liveIndicator.isVisible = true - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) - val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) recordButton.setImageDrawable(drawable) @@ -102,9 +73,6 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem { recordButton.isEnabled = false stopRecordButton.isEnabled = false - liveIndicator.isVisible = false } } } @@ -126,10 +93,9 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem(R.id.liveIndicator) - val roomAvatarImageView by bind(R.id.roomAvatarImageView) - val titleText by bind(R.id.titleText) + class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) { + val listenersCountMetadata by bind(R.id.listenersCountMetadata) + val remainingTimeMetadata by bind(R.id.remainingTimeMetadata) val recordButton by bind(R.id.recordButton) val stopRecordButton by bind(R.id.stopRecordButton) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt index 2c892c8306..6545948021 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt @@ -82,10 +82,17 @@ class VoiceBroadcastPlayer @Inject constructor( set(value) { Timber.w("## VoiceBroadcastPlayer state: $field -> $value") field = value - listeners.forEach { it.onStateChanged(value) } + // Notify state change to all the listeners attached to the current voice broadcast id + currentVoiceBroadcastId?.let { voiceBroadcastId -> + listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } + } } private var currentRoomId: String? = null - private var listeners = CopyOnWriteArrayList() + + /** + * Map voiceBroadcastId to listeners + */ + private var listeners: MutableMap> = mutableMapOf() fun playOrResume(roomId: String, eventId: String) { val hasChanged = currentVoiceBroadcastId != eventId @@ -133,13 +140,21 @@ class VoiceBroadcastPlayer @Inject constructor( currentVoiceBroadcastId = null } - fun addListener(listener: Listener) { - listeners.add(listener) - listener.onStateChanged(state) + /** + * Add a [Listener] to the given voice broadcast id. + */ + fun addListener(voiceBroadcastId: String, listener: Listener) { + listeners[voiceBroadcastId]?.add(listener) ?: run { + listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } + } + if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(state) else listener.onStateChanged(State.IDLE) } - fun removeListener(listener: Listener) { - listeners.remove(listener) + /** + * Remove a [Listener] from the given voice broadcast id. + */ + fun removeListener(voiceBroadcastId: String, listener: Listener) { + listeners[voiceBroadcastId]?.remove(listener) } private fun startPlayback(roomId: String, eventId: String) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt new file mode 100644 index 0000000000..e142cb15ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.views + +import android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.core.content.res.use +import im.vector.app.R +import im.vector.app.databinding.ViewVoiceBroadcastMetadataBinding + +class VoiceBroadcastMetadataView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val views = ViewVoiceBroadcastMetadataBinding.inflate( + LayoutInflater.from(context), + this + ) + + var value: String + get() = views.metadataValue.text.toString() + set(newValue) { + views.metadataValue.text = newValue + } + + init { + context.obtainStyledAttributes( + attrs, + R.styleable.VoiceBroadcastMetadataView, + 0, + 0 + ).use { + setIcon(it) + setValue(it) + } + } + + private fun setIcon(typedArray: TypedArray) { + val icon = typedArray.getDrawable(R.styleable.VoiceBroadcastMetadataView_metadataIcon) + views.metadataIcon.setImageDrawable(icon) + } + + private fun setValue(typedArray: TypedArray) { + val value = typedArray.getString(R.styleable.VoiceBroadcastMetadataView_metadataValue) + views.metadataValue.text = value + } +} diff --git a/vector/src/main/res/drawable/ic_timer.xml b/vector/src/main/res/drawable/ic_timer.xml new file mode 100644 index 0000000000..11a42b0696 --- /dev/null +++ b/vector/src/main/res/drawable/ic_timer.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_16.xml b/vector/src/main/res/drawable/ic_voice_broadcast_16.xml deleted file mode 100644 index 7d427a56d0..0000000000 --- a/vector/src/main/res/drawable/ic_voice_broadcast_16.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml b/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml new file mode 100644 index 0000000000..edadb55b81 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_broadcast_mic.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 97f15967e1..16a5b17d68 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -7,8 +7,7 @@ android:layout_height="wrap_content" android:background="@drawable/rounded_rect_shape_8" android:backgroundTint="?vctr_content_quinary" - android:padding="@dimen/layout_vertical_margin" - tools:viewBindingIgnore="true"> + android:padding="@dimen/layout_vertical_margin"> @@ -54,61 +53,41 @@ android:contentDescription="@string/avatar" app:layout_constraintStart_toEndOf="@id/avatarRightBarrier" app:layout_constraintTop_toTopOf="parent" - tools:src="@sample/rooms.json/data/name" /> + tools:text="@sample/rooms.json/data/name" /> - - - + app:layout_constraintTop_toBottomOf="@id/titleText" /> - - - - + app:metadataIcon="@drawable/ic_voice_broadcast_mic" + tools:metadataValue="@sample/users.json/data/displayName" /> - + - - + + app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" /> + android:padding="@dimen/layout_vertical_margin"> @@ -54,7 +53,34 @@ android:contentDescription="@string/avatar" app:layout_constraintStart_toEndOf="@id/avatarRightBarrier" app:layout_constraintTop_toTopOf="parent" - tools:src="@sample/users.json/data/displayName" /> + tools:text="@sample/users.json/data/displayName" /> + + + + + + + app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" /> + + + + + + From 4defc3dded84ac411544e5d8f8f8173becbc4509 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 14:50:56 +0200 Subject: [PATCH 003/679] Voice Broadcast - Add style for the "live" indicator --- .../res/values/styles_voice_broadcast.xml | 19 +++++++++++++++++ .../main/res/drawable/ic_voice_broadcast.xml | 21 +++++++++++++++++++ ...e_event_voice_broadcast_listening_stub.xml | 16 +++----------- ...e_event_voice_broadcast_recording_stub.xml | 14 ++----------- .../layout/view_voice_broadcast_metadata.xml | 2 +- 5 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 library/ui-styles/src/main/res/values/styles_voice_broadcast.xml create mode 100644 vector/src/main/res/drawable/ic_voice_broadcast.xml diff --git a/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml b/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml new file mode 100644 index 0000000000..eb85378141 --- /dev/null +++ b/library/ui-styles/src/main/res/values/styles_voice_broadcast.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_voice_broadcast.xml b/vector/src/main/res/drawable/ic_voice_broadcast.xml new file mode 100644 index 0000000000..7d427a56d0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_broadcast.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 16a5b17d68..d508569cb0 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -11,20 +11,10 @@ @@ -78,7 +68,7 @@ android:id="@+id/voiceBroadcastMetadata" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:metadataIcon="@drawable/ic_attachment_voice_broadcast" + app:metadataIcon="@drawable/ic_voice_broadcast" app:metadataValue="@string/attachment_type_voice_broadcast" /> diff --git a/vector/src/main/res/layout/view_voice_broadcast_metadata.xml b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml index 4f0c584d5c..3bc31cd9a0 100644 --- a/vector/src/main/res/layout/view_voice_broadcast_metadata.xml +++ b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml @@ -15,7 +15,7 @@ android:layout_marginEnd="4dp" android:contentDescription="@null" app:tint="?vctr_content_secondary" - tools:src="@drawable/ic_attachment_voice_broadcast" /> + tools:src="@drawable/ic_voice_broadcast" /> Date: Mon, 24 Oct 2022 16:35:16 +0200 Subject: [PATCH 004/679] Improve VoiceBroadcastItemFactory --- .../factory/VoiceBroadcastItemFactory.kt | 48 +++++++++---------- .../timeline/helper/TimelineEventsGroups.kt | 3 ++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 7b8c927186..b639a2dbae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -15,14 +15,14 @@ */ package im.vector.app.features.home.room.detail.timeline.factory -import im.vector.app.core.epoxy.VectorEpoxyHolder -import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider +import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.app.features.home.room.detail.timeline.item.AbsMessageVoiceBroadcastItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem @@ -34,7 +34,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.getUser +import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -53,31 +53,31 @@ class VoiceBroadcastItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, - ): VectorEpoxyModel? { + ): AbsMessageVoiceBroadcastItem<*>? { // Only display item of the initial event with updated data if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null - val eventsGroup = params.eventsGroup ?: return null - val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup) - val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent() - val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent() - val mostRecentMessageContent = mostRecentEvent?.content ?: return null - val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId - val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey + + val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null + val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null + val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null + val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId + + val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId + return if (isRecording) { createRecordingItem( - params.event.roomId, - eventsGroup.groupId, - mostRecentMessageContent.voiceBroadcastState, + params, + voiceBroadcastId, + voiceBroadcastContent.voiceBroadcastState, highlight, callback, attributes ) } else { createListeningItem( - params.event.roomId, - eventsGroup.groupId, - mostRecentMessageContent.voiceBroadcastState, - recorderName, + params, + voiceBroadcastId, + voiceBroadcastContent.voiceBroadcastState, highlight, callback, attributes @@ -86,14 +86,14 @@ class VoiceBroadcastItemFactory @Inject constructor( } private fun createRecordingItem( - roomId: String, + params: TimelineItemFactoryParams, voiceBroadcastId: String, voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, ): MessageVoiceBroadcastRecordingItem { - val roomSummary = session.getRoom(roomId)?.roomSummary() + val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() return MessageVoiceBroadcastRecordingItem_() .id("voice_broadcast_$voiceBroadcastId") .attributes(attributes) @@ -109,15 +109,15 @@ class VoiceBroadcastItemFactory @Inject constructor( } private fun createListeningItem( - roomId: String, + params: TimelineItemFactoryParams, voiceBroadcastId: String, voiceBroadcastState: VoiceBroadcastState?, - broadcasterName: String?, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, ): MessageVoiceBroadcastListeningItem { - val roomSummary = session.getRoom(roomId)?.roomSummary() + val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() + val recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName() return MessageVoiceBroadcastListeningItem_() .id("voice_broadcast_$voiceBroadcastId") .attributes(attributes) @@ -128,7 +128,7 @@ class VoiceBroadcastItemFactory @Inject constructor( .voiceBroadcastPlayer(voiceBroadcastPlayer) .voiceBroadcastId(voiceBroadcastId) .voiceBroadcastState(voiceBroadcastState) - .broadcasterName(broadcasterName) + .broadcasterName(recorderName) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(callback) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index d8817c1f44..8a3be7d5f2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -141,6 +141,9 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) { } class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { + + val voiceBroadcastId = group.groupId + fun getLastDisplayableEvent(): TimelineEvent { return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } ?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L } From 2c144614cabc6427319ebfcb3143ab176b6d565a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 16:49:59 +0200 Subject: [PATCH 005/679] Improve recording state rendering if app has been relaunched --- .../MessageVoiceBroadcastRecordingItem.kt | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index b766698851..183d2a5577 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -24,6 +24,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass @@ -32,7 +33,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem @EpoxyAttribute var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null - private lateinit var recorderListener: VoiceBroadcastRecorder.Listener + private var recorderListener: VoiceBroadcastRecorder.Listener? = null override fun bind(holder: Holder) { super.bind(holder) @@ -40,12 +41,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - recorderListener = object : VoiceBroadcastRecorder.Listener { - override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { - renderRecordingState(holder, state) - } + if (voiceBroadcastRecorder != null && voiceBroadcastRecorder?.state != VoiceBroadcastRecorder.State.Idle) { + recorderListener = object : VoiceBroadcastRecorder.Listener { + override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { + renderRecordingState(holder, state) + } + }.also { voiceBroadcastRecorder?.addListener(it) } + } else { + renderVoiceBroadcastState(holder) } - voiceBroadcastRecorder?.addListener(recorderListener) } override fun renderMetadata(holder: Holder) { @@ -56,39 +60,54 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) { - with(holder) { - when (state) { - VoiceBroadcastRecorder.State.Recording -> { - stopRecordButton.isEnabled = true - recordButton.isEnabled = true - - val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) - val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) - recordButton.setImageDrawable(drawable) - recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } - } - VoiceBroadcastRecorder.State.Paused -> { - stopRecordButton.isEnabled = true - recordButton.isEnabled = true - - recordButton.setImageResource(R.drawable.ic_recording_dot) - recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } - } - VoiceBroadcastRecorder.State.Idle -> { - recordButton.isEnabled = false - stopRecordButton.isEnabled = false - } - } + when (state) { + VoiceBroadcastRecorder.State.Recording -> renderPlayingState(holder) + VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder) + VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder) + } + } + + private fun renderVoiceBroadcastState(holder: Holder) { + when (voiceBroadcastState) { + VoiceBroadcastState.STARTED, + VoiceBroadcastState.RESUMED -> renderPlayingState(holder) + VoiceBroadcastState.PAUSED -> renderPausedState(holder) + VoiceBroadcastState.STOPPED, + null -> renderStoppedState(holder) } } + private fun renderPlayingState(holder: Holder) = with(holder) { + stopRecordButton.isEnabled = true + recordButton.isEnabled = true + + val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) + recordButton.setImageDrawable(drawable) + recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record) + recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } + stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + } + + private fun renderPausedState(holder: Holder) = with(holder) { + stopRecordButton.isEnabled = true + recordButton.isEnabled = true + + recordButton.setImageResource(R.drawable.ic_recording_dot) + recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record) + recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } + stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + } + + private fun renderStoppedState(holder: Holder) = with(holder) { + recordButton.isEnabled = false + stopRecordButton.isEnabled = false + } + override fun unbind(holder: Holder) { super.unbind(holder) - voiceBroadcastRecorder?.removeListener(recorderListener) + recorderListener?.let { voiceBroadcastRecorder?.removeListener(it) } + recorderListener = null } override fun getViewStubId() = STUB_ID From f31429cf25ca232344715d7a39906472e2e290be Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 15:22:16 +0200 Subject: [PATCH 006/679] Rename renderLiveIcon method --- .../room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index cbf35e89d2..afe705ffb6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -63,11 +63,11 @@ abstract class AbsMessageVoiceBroadcastItem Date: Tue, 25 Oct 2022 16:29:42 +0200 Subject: [PATCH 007/679] Move voice broadcast item attributes to dedicated class --- .../timeline/factory/MessageItemFactory.kt | 2 +- .../factory/VoiceBroadcastItemFactory.kt | 65 ++++++------------- .../item/AbsMessageVoiceBroadcastItem.kt | 42 +++++++----- .../MessageVoiceBroadcastListeningItem.kt | 17 ++--- .../MessageVoiceBroadcastRecordingItem.kt | 18 ++--- 5 files changed, 57 insertions(+), 87 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 245d92f95b..f4d506fa4b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -201,7 +201,7 @@ class MessageItemFactory @Inject constructor( is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) - is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes) + is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } return messageItem?.apply { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index b639a2dbae..d43ccd9834 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.displayname.getBestName -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem @@ -51,7 +50,6 @@ class VoiceBroadcastItemFactory @Inject constructor( params: TimelineItemFactoryParams, messageContent: MessageVoiceBroadcastInfoContent, highlight: Boolean, - callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, ): AbsMessageVoiceBroadcastItem<*>? { // Only display item of the initial event with updated data @@ -64,72 +62,47 @@ class VoiceBroadcastItemFactory @Inject constructor( val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId + val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( + voiceBroadcastId = voiceBroadcastId, + voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, + recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), + recorder = voiceBroadcastRecorder, + player = voiceBroadcastPlayer, + roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(), + colorProvider = colorProvider, + drawableProvider = drawableProvider, + ) + return if (isRecording) { - createRecordingItem( - params, - voiceBroadcastId, - voiceBroadcastContent.voiceBroadcastState, - highlight, - callback, - attributes - ) + createRecordingItem(highlight, attributes, voiceBroadcastAttributes) } else { - createListeningItem( - params, - voiceBroadcastId, - voiceBroadcastContent.voiceBroadcastState, - highlight, - callback, - attributes - ) + createListeningItem(highlight, attributes, voiceBroadcastAttributes) } } private fun createRecordingItem( - params: TimelineItemFactoryParams, - voiceBroadcastId: String, - voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, - callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, + voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastRecordingItem { - val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() return MessageVoiceBroadcastRecordingItem_() - .id("voice_broadcast_$voiceBroadcastId") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") .attributes(attributes) + .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) - .roomItem(roomSummary?.toMatrixItem()) - .colorProvider(colorProvider) - .drawableProvider(drawableProvider) - .voiceBroadcastRecorder(voiceBroadcastRecorder) - .voiceBroadcastId(voiceBroadcastId) - .voiceBroadcastState(voiceBroadcastState) .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) } private fun createListeningItem( - params: TimelineItemFactoryParams, - voiceBroadcastId: String, - voiceBroadcastState: VoiceBroadcastState?, highlight: Boolean, - callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, + voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastListeningItem { - val roomSummary = session.getRoom(params.event.roomId)?.roomSummary() - val recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName() return MessageVoiceBroadcastListeningItem_() - .id("voice_broadcast_$voiceBroadcastId") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") .attributes(attributes) + .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) - .roomItem(roomSummary?.toMatrixItem()) - .colorProvider(colorProvider) - .drawableProvider(drawableProvider) - .voiceBroadcastPlayer(voiceBroadcastPlayer) - .voiceBroadcastId(voiceBroadcastId) - .voiceBroadcastState(voiceBroadcastState) - .broadcasterName(recorderName) .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index afe705ffb6..45f10b68d0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -25,29 +25,26 @@ import im.vector.app.R import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import org.matrix.android.sdk.api.util.MatrixItem abstract class AbsMessageVoiceBroadcastItem : AbsMessageItem() { @EpoxyAttribute - var callback: TimelineEventController.Callback? = null + lateinit var voiceBroadcastAttributes: Attributes - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - lateinit var drawableProvider: DrawableProvider - - @EpoxyAttribute - lateinit var voiceBroadcastId: String - - @EpoxyAttribute - var voiceBroadcastState: VoiceBroadcastState? = null - - @EpoxyAttribute - var roomItem: MatrixItem? = null + protected val voiceBroadcastId get() = voiceBroadcastAttributes.voiceBroadcastId + protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState + protected val recorderName get() = voiceBroadcastAttributes.recorderName + protected val recorder get() = voiceBroadcastAttributes.recorder + protected val player get() = voiceBroadcastAttributes.player + protected val roomItem get() = voiceBroadcastAttributes.roomItem + protected val colorProvider get() = voiceBroadcastAttributes.colorProvider + protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider + protected val avatarRenderer get() = attributes.avatarRenderer + protected val callback get() = attributes.callback override fun isCacheable(): Boolean = false @@ -59,7 +56,7 @@ abstract class AbsMessageVoiceBroadcastItem(R.id.roomAvatarImageView) val titleText by bind(R.id.titleText) } + + data class Attributes( + val voiceBroadcastId: String, + val voiceBroadcastState: VoiceBroadcastState?, + val recorderName: String, + val recorder: VoiceBroadcastRecorder?, + val player: VoiceBroadcastPlayer, + val roomItem: MatrixItem?, + val colorProvider: ColorProvider, + val drawableProvider: DrawableProvider, + ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 135053d9a9..d94bee3672 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageButton import androidx.core.view.isVisible -import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick @@ -30,12 +29,6 @@ import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem() { - @EpoxyAttribute - var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null - - @EpoxyAttribute - var broadcasterName: String? = null - private lateinit var playerListener: VoiceBroadcastPlayer.Listener override fun bind(holder: Holder) { @@ -47,12 +40,12 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playerListener = VoiceBroadcastPlayer.Listener { state -> renderPlayingState(holder, state) } - voiceBroadcastPlayer?.addListener(voiceBroadcastId, playerListener) + player.addListener(voiceBroadcastId, playerListener) } override fun renderMetadata(holder: Holder) { with(holder) { - broadcasterNameMetadata.value = broadcasterName.orEmpty() + broadcasterNameMetadata.value = recorderName voiceBroadcastMetadata.isVisible = true listenersCountMetadata.isVisible = false } @@ -67,14 +60,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } + playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { - attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) + callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } } VoiceBroadcastPlayer.State.BUFFERING -> Unit @@ -84,7 +77,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) - voiceBroadcastPlayer?.removeListener(voiceBroadcastId, playerListener) + player.removeListener(voiceBroadcastId, playerListener) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 183d2a5577..47e89658ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.item import android.widget.ImageButton import androidx.core.view.isVisible -import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick @@ -30,9 +29,6 @@ import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem() { - @EpoxyAttribute - var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null - private var recorderListener: VoiceBroadcastRecorder.Listener? = null override fun bind(holder: Holder) { @@ -41,12 +37,12 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - if (voiceBroadcastRecorder != null && voiceBroadcastRecorder?.state != VoiceBroadcastRecorder.State.Idle) { + if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) { recorderListener = object : VoiceBroadcastRecorder.Listener { override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { renderRecordingState(holder, state) } - }.also { voiceBroadcastRecorder?.addListener(it) } + }.also { recorder?.addListener(it) } } else { renderVoiceBroadcastState(holder) } @@ -85,8 +81,8 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor) recordButton.setImageDrawable(drawable) recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) } + stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } } private fun renderPausedState(holder: Holder) = with(holder) { @@ -95,8 +91,8 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem recordButton.setImageResource(R.drawable.ic_recording_dot) recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record) - recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } - stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } + recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) } + stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) } } private fun renderStoppedState(holder: Holder) = with(holder) { @@ -106,7 +102,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) - recorderListener?.let { voiceBroadcastRecorder?.removeListener(it) } + recorderListener?.let { recorder?.removeListener(it) } recorderListener = null } From 513097585a7ab9592eb50d5172e30eaf3c428406 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:38:05 +0200 Subject: [PATCH 008/679] Fix kdoc issue --- .../vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt index 6545948021..d8a062c8f8 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt @@ -90,7 +90,7 @@ class VoiceBroadcastPlayer @Inject constructor( private var currentRoomId: String? = null /** - * Map voiceBroadcastId to listeners + * Map voiceBroadcastId to listeners. */ private var listeners: MutableMap> = mutableMapOf() From 0f21f404e694a465cfa82a0da8e178dc9a0af821 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:41:36 +0200 Subject: [PATCH 009/679] Add changelog --- changelog.d/7448.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7448.wip diff --git a/changelog.d/7448.wip b/changelog.d/7448.wip new file mode 100644 index 0000000000..a99e5bbcfa --- /dev/null +++ b/changelog.d/7448.wip @@ -0,0 +1 @@ +[Voice Broadcast] Improve timeline items factory and handle bad recording state display From c7c05d1fe6a293b9233276a8b3d2c68628ecd1e3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 16:35:59 +0200 Subject: [PATCH 010/679] Add check on deviceId before showing recording tile --- .../room/detail/timeline/factory/VoiceBroadcastItemFactory.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index d43ccd9834..7a7cb73471 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -60,7 +60,9 @@ class VoiceBroadcastItemFactory @Inject constructor( val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId - val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId + val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && + voiceBroadcastEvent.root.stateKey == session.myUserId && + messageContent.deviceId == session.sessionParams.deviceId val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( voiceBroadcastId = voiceBroadcastId, From a4eff0cc78d8066ca0fa31a13028b7a81b3c31ed Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:56:27 +0200 Subject: [PATCH 011/679] Add changelog --- changelog.d/7431.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7431.bugfix diff --git a/changelog.d/7431.bugfix b/changelog.d/7431.bugfix new file mode 100644 index 0000000000..681a1e9aa5 --- /dev/null +++ b/changelog.d/7431.bugfix @@ -0,0 +1 @@ + [Voice Broadcast] Do not display the recorder view for a live broadcast started from another session \ No newline at end of file From 6eeb54ae40dbd995a55822659c40365d288e0727 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 24 Oct 2022 23:59:29 +0200 Subject: [PATCH 012/679] Stop ongoing voice broadcast on app restart --- .../features/home/HomeActivityViewModel.kt | 30 +++++++++++++ .../voicebroadcast/VoiceBroadcastHelper.kt | 4 ++ .../usecase/GetLastVoiceBroadcastUseCase.kt | 45 +++++++++++++++++++ .../usecase/StartVoiceBroadcastUseCase.kt | 10 +---- 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 61a8e5b79e..1e79dc5844 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -42,6 +42,8 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -60,12 +62,14 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.room.model.Membership @@ -92,6 +96,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val analyticsConfig: AnalyticsConfig, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, private val vectorFeatures: VectorFeatures, + private val voiceBroadcastHelper: VoiceBroadcastHelper, ) : VectorViewModel(initialState) { @AssistedFactory @@ -123,6 +128,7 @@ class HomeActivityViewModel @AssistedInject constructor( observeReleaseNotes() observeLocalNotificationsSilenced() initThreadsMigration() + stopOngoingVoiceBroadcast() } private fun observeReleaseNotes() = withState { state -> @@ -490,6 +496,30 @@ class HomeActivityViewModel @AssistedInject constructor( } } + /** + * Stop ongoing voice broadcast if any. + */ + private fun stopOngoingVoiceBroadcast() { + val session = activeSessionHolder.getSafeActiveSession() ?: return + + // FIXME Iterate only on recent rooms for the moment, improve this + val recentRooms = session.roomService().getBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }).mapNotNull { session.getRoom(it.roomId) } + + recentRooms + .forEach { room -> + val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) + val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId + val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } + if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { + viewModelScope.launch { voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) } + return // No need to iterate more as we should not have more than one recording VB + } + } + } + override fun handle(action: HomeActivityViewActions) { when (action) { HomeActivityViewActions.PushPromptHasBeenReviewed -> { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 58e7de7f32..ee9034661c 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -16,6 +16,7 @@ package im.vector.app.features.voicebroadcast +import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase @@ -30,6 +31,7 @@ class VoiceBroadcastHelper @Inject constructor( private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase, private val resumeVoiceBroadcastUseCase: ResumeVoiceBroadcastUseCase, private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, ) { suspend fun startVoiceBroadcast(roomId: String) = startVoiceBroadcastUseCase.execute(roomId) @@ -45,4 +47,6 @@ class VoiceBroadcastHelper @Inject constructor( fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() + + fun getOngoingVoiceBroadcasts(roomId: String) = getOngoingVoiceBroadcastsUseCase.execute(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt new file mode 100644 index 0000000000..db2c625161 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.usecase + +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom +import timber.log.Timber +import javax.inject.Inject + +class GetOngoingVoiceBroadcastsUseCase @Inject constructor( + private val session: Session, +) { + + fun execute(roomId: String): List { + val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") + + Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") + + return room.stateService().getStateEvents( + setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), + QueryStringValue.IsNotEmpty + ) + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt index 7934d18e36..2b7ca7b9f1 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt @@ -25,9 +25,7 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.lib.multipicker.utils.toMultiPickerAudioType -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toContent @@ -43,6 +41,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val context: Context, private val buildMeta: BuildMeta, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, ) { suspend fun execute(roomId: String): Result = runCatching { @@ -50,12 +49,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") - val onGoingVoiceBroadcastEvents = room.stateService().getStateEvents( - setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), - QueryStringValue.IsNotEmpty - ) - .mapNotNull { it.asVoiceBroadcastEvent() } - .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId) if (onGoingVoiceBroadcastEvents.isEmpty()) { startVoiceBroadcast(room) From 53db04c8cf28d3b3ce2993367f1df986eccad962 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 25 Oct 2022 17:58:09 +0200 Subject: [PATCH 013/679] Add changelog --- changelog.d/7450.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7450.wip diff --git a/changelog.d/7450.wip b/changelog.d/7450.wip new file mode 100644 index 0000000000..de4d3dc5e1 --- /dev/null +++ b/changelog.d/7450.wip @@ -0,0 +1 @@ +[Voice Broadcast] Stop recording when opening the room after an app restart From 85bc78bd72f15b2741b02b3a6f8389e0d4474761 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 09:50:58 +0200 Subject: [PATCH 014/679] Do not pause already paused voice broadcast --- .../home/room/detail/composer/MessageComposerFragment.kt | 2 +- .../home/room/detail/composer/MessageComposerViewState.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 55ec922a57..e01dd31516 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -234,7 +234,7 @@ class MessageComposerFragment : VectorBaseFragment(), A } // TODO remove this when there will be a recording indicator outside of the timeline // Pause voice broadcast if the timeline is not shown anymore - it.isVoiceBroadcasting && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause) + it.isRecordingVoiceBroadcast && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause) else -> { timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause) messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString())) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 0df1dbebd8..a4021f87b2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -79,9 +79,8 @@ data class MessageComposerViewState( is VoiceMessageRecorderView.RecordingUiState.Recording -> true } - val isVoiceBroadcasting = when (voiceBroadcastState) { + val isRecordingVoiceBroadcast = when (voiceBroadcastState) { VoiceBroadcastState.STARTED, - VoiceBroadcastState.PAUSED, VoiceBroadcastState.RESUMED -> true else -> false } From 47047b20349c423b6f070e78797ffbbd599d764c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:00:56 +0200 Subject: [PATCH 015/679] move map operator in a new line --- .../vector/app/features/home/HomeActivityViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 1e79dc5844..2c45709291 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -503,10 +503,12 @@ class HomeActivityViewModel @AssistedInject constructor( val session = activeSessionHolder.getSafeActiveSession() ?: return // FIXME Iterate only on recent rooms for the moment, improve this - val recentRooms = session.roomService().getBreadcrumbs(roomSummaryQueryParams { - displayName = QueryStringValue.NoCondition - memberships = listOf(Membership.JOIN) - }).mapNotNull { session.getRoom(it.roomId) } + val recentRooms = session.roomService() + .getBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) + .mapNotNull { session.getRoom(it.roomId) } recentRooms .forEach { room -> From ec80adc8aa416c07ebd61faff5cab5c26e7ccff4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:10:56 +0200 Subject: [PATCH 016/679] Rename usecase file --- ...iceBroadcastUseCase.kt => GetOngoingVoiceBroadcastsUseCase.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetLastVoiceBroadcastUseCase.kt => GetOngoingVoiceBroadcastsUseCase.kt} (100%) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt similarity index 100% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetLastVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt From 6091ec4ce3731b6498c42860d1d82dce677c2a8d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:45:25 +0200 Subject: [PATCH 017/679] Fix wrong content description --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index d94bee3672..a3e7cc55d5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -59,13 +59,13 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem when (state) { VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) - playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) + playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) - playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) + playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } From 8fe3b5e75077d21b32cf6c2ba82fc7c9e47200d7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:46:33 +0200 Subject: [PATCH 018/679] Rename method renderPlayingState to renderRecordingState --- .../timeline/item/MessageVoiceBroadcastRecordingItem.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 47e89658ca..e3e86f38e3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -57,7 +57,7 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) { when (state) { - VoiceBroadcastRecorder.State.Recording -> renderPlayingState(holder) + VoiceBroadcastRecorder.State.Recording -> renderRecordingState(holder) VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder) VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder) } @@ -66,14 +66,14 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem private fun renderVoiceBroadcastState(holder: Holder) { when (voiceBroadcastState) { VoiceBroadcastState.STARTED, - VoiceBroadcastState.RESUMED -> renderPlayingState(holder) + VoiceBroadcastState.RESUMED -> renderRecordingState(holder) VoiceBroadcastState.PAUSED -> renderPausedState(holder) VoiceBroadcastState.STOPPED, null -> renderStoppedState(holder) } } - private fun renderPlayingState(holder: Holder) = with(holder) { + private fun renderRecordingState(holder: Holder) = with(holder) { stopRecordButton.isEnabled = true recordButton.isEnabled = true From 1554d79f1a57095f3f98b20ffee8e944e7d60374 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 10:48:11 +0200 Subject: [PATCH 019/679] Change listeners Map variable to immutable --- .../vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt index d8a062c8f8..5a04904f69 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt @@ -92,7 +92,7 @@ class VoiceBroadcastPlayer @Inject constructor( /** * Map voiceBroadcastId to listeners. */ - private var listeners: MutableMap> = mutableMapOf() + private val listeners: MutableMap> = mutableMapOf() fun playOrResume(roomId: String, eventId: String) { val hasChanged = currentVoiceBroadcastId != eventId From 2f14d191302b581a37fc6cd3cc5846d1009530b9 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 11:26:12 +0200 Subject: [PATCH 020/679] Fix failing test --- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 9fa6b7a450..f95ab2053b 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -49,10 +49,11 @@ class StartVoiceBroadcastUseCaseTest { private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( - fakeSession, - fakeVoiceBroadcastRecorder, - FakeContext().instance, - mockk() + session = fakeSession, + voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, + context = FakeContext().instance, + buildMeta = mockk(), + getOngoingVoiceBroadcastsUseCase = GetOngoingVoiceBroadcastsUseCase(fakeSession), ) @Test From 5855fe1242d7e99317b79435a541bf2bd68980ef Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 12:44:18 +0200 Subject: [PATCH 021/679] Add StopOngoingVoiceBroadcastUseCase --- .../features/home/HomeActivityViewModel.kt | 35 +---------- .../StopOngoingVoiceBroadcastUseCase.kt | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 2c45709291..c3abdde022 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -42,8 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.usecase.StopOngoingVoiceBroadcastUseCase import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -62,14 +61,12 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.room.model.Membership @@ -96,7 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val analyticsConfig: AnalyticsConfig, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, private val vectorFeatures: VectorFeatures, - private val voiceBroadcastHelper: VoiceBroadcastHelper, + private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -128,7 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor( observeReleaseNotes() observeLocalNotificationsSilenced() initThreadsMigration() - stopOngoingVoiceBroadcast() + viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() } } private fun observeReleaseNotes() = withState { state -> @@ -496,32 +493,6 @@ class HomeActivityViewModel @AssistedInject constructor( } } - /** - * Stop ongoing voice broadcast if any. - */ - private fun stopOngoingVoiceBroadcast() { - val session = activeSessionHolder.getSafeActiveSession() ?: return - - // FIXME Iterate only on recent rooms for the moment, improve this - val recentRooms = session.roomService() - .getBreadcrumbs(roomSummaryQueryParams { - displayName = QueryStringValue.NoCondition - memberships = listOf(Membership.JOIN) - }) - .mapNotNull { session.getRoom(it.roomId) } - - recentRooms - .forEach { room -> - val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) - val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId - val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } - if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { - viewModelScope.launch { voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) } - return // No need to iterate more as we should not have more than one recording VB - } - } - } - override fun handle(action: HomeActivityViewActions) { when (action) { HomeActivityViewActions.PushPromptHasBeenReviewed -> { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt new file mode 100644 index 0000000000..82baa5e6a8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.usecase + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import timber.log.Timber +import javax.inject.Inject + +/** + * Stop ongoing voice broadcast if any. + */ +class StopOngoingVoiceBroadcastUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val voiceBroadcastHelper: VoiceBroadcastHelper, +) { + + suspend fun execute() { + Timber.d("## StopOngoingVoiceBroadcastUseCase: Stop ongoing voice broadcast requested") + + val session = activeSessionHolder.getSafeActiveSession() ?: run { + Timber.w("## StopOngoingVoiceBroadcastUseCase: no active session") + return + } + // FIXME Iterate only on recent rooms for the moment, improve this + val recentRooms = session.roomService() + .getBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) + .mapNotNull { session.getRoom(it.roomId) } + + recentRooms + .forEach { room -> + val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) + val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId + val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } + if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { + voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) + return // No need to iterate more as we should not have more than one recording VB + } + } + } +} From 443d573205bec14e5a7f12d0220da0a659194f4c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 12:48:32 +0200 Subject: [PATCH 022/679] Remove getOngoingVoiceBroadcasts from VoiceBroadcastHelper --- .../app/features/voicebroadcast/VoiceBroadcastHelper.kt | 4 ---- .../usecase/StopOngoingVoiceBroadcastUseCase.kt | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index ee9034661c..58e7de7f32 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -16,7 +16,6 @@ package im.vector.app.features.voicebroadcast -import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase @@ -31,7 +30,6 @@ class VoiceBroadcastHelper @Inject constructor( private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase, private val resumeVoiceBroadcastUseCase: ResumeVoiceBroadcastUseCase, private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, - private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, ) { suspend fun startVoiceBroadcast(roomId: String) = startVoiceBroadcastUseCase.execute(roomId) @@ -47,6 +45,4 @@ class VoiceBroadcastHelper @Inject constructor( fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() - - fun getOngoingVoiceBroadcasts(roomId: String) = getOngoingVoiceBroadcastsUseCase.execute(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt index 82baa5e6a8..ab4d16ab60 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -31,6 +31,7 @@ import javax.inject.Inject */ class StopOngoingVoiceBroadcastUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, private val voiceBroadcastHelper: VoiceBroadcastHelper, ) { @@ -51,7 +52,7 @@ class StopOngoingVoiceBroadcastUseCase @Inject constructor( recentRooms .forEach { room -> - val ongoingVoiceBroadcasts = voiceBroadcastHelper.getOngoingVoiceBroadcasts(room.roomId) + val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId) val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { From 23b4f6d42f467d9257f8aae068941ab566af6afb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 12:49:51 +0200 Subject: [PATCH 023/679] Inject ActiveSessionHolder in GetOngoingVoiceBroadcastsUseCase --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index db2c625161..47a9ed7b4a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -16,21 +16,22 @@ package im.vector.app.features.voicebroadcast.usecase +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import timber.log.Timber import javax.inject.Inject class GetOngoingVoiceBroadcastsUseCase @Inject constructor( - private val session: Session, + private val activeSessionHolder: ActiveSessionHolder, ) { fun execute(roomId: String): List { + val session = activeSessionHolder.getSafeActiveSession() ?: return emptyList() val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") From 0cc2a477b473aeaec0183add3dd3243fc2c49a18 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 14:54:55 +0200 Subject: [PATCH 024/679] Mockk GetOngoingVoiceBroadcastsUseCase and adapt tests --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 8 ++++++-- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 16 ++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index 47a9ed7b4a..cb228ad8aa 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -31,8 +31,12 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor( ) { fun execute(roomId: String): List { - val session = activeSessionHolder.getSafeActiveSession() ?: return emptyList() - val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") + println("## GetOngoingVoiceBroadcastsUseCase") + println("## GetOngoingVoiceBroadcastsUseCase activeSessionHolder $activeSessionHolder") + val session = activeSessionHolder.getSafeActiveSession() + println("## GetOngoingVoiceBroadcastsUseCase session $session") + val room = session?.getRoom(roomId) ?: error("Unknown roomId: $roomId") + println("## GetOngoingVoiceBroadcastsUseCase room $room") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index f95ab2053b..217a395076 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -20,6 +20,7 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService @@ -27,13 +28,13 @@ import im.vector.app.test.fakes.FakeSession import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import io.mockk.slot import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeNull import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -48,12 +49,13 @@ class StartVoiceBroadcastUseCaseTest { private val fakeRoom = FakeRoom() private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) + private val fakeGetOngoingVoiceBroadcastsUseCase = mockk() private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( session = fakeSession, voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, context = FakeContext().instance, buildMeta = mockk(), - getOngoingVoiceBroadcastsUseCase = GetOngoingVoiceBroadcastsUseCase(fakeSession), + getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, ) @Test @@ -81,7 +83,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List) { // Given clearAllMocks() - givenAVoiceBroadcasts(voiceBroadcasts) + givenVoiceBroadcasts(voiceBroadcasts) val voiceBroadcastInfoContentInterceptor = slot() coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID } @@ -104,7 +106,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List) { // Given clearAllMocks() - givenAVoiceBroadcasts(voiceBroadcasts) + givenVoiceBroadcasts(voiceBroadcasts) // When startVoiceBroadcastUseCase.execute(A_ROOM_ID) @@ -113,7 +115,7 @@ class StartVoiceBroadcastUseCaseTest { coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) } } - private fun givenAVoiceBroadcasts(voiceBroadcasts: List) { + private fun givenVoiceBroadcasts(voiceBroadcasts: List) { val events = voiceBroadcasts.map { Event( type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, @@ -123,7 +125,9 @@ class StartVoiceBroadcastUseCaseTest { ).toContent() ) } - fakeRoom.stateService().givenGetStateEvents(QueryStringValue.IsNotEmpty, events) + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events } private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState) From d242ab049b0c5c09ab1ea0f8b3dda2aa805472a0 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 26 Oct 2022 15:15:48 +0200 Subject: [PATCH 025/679] [Rich text editor] Implement full screen editor mode (simple approach) (#7436) * Rich text editor: implement full screen editor mode using ConstraintSets * Add back press handler * Change ToggleFullScreen to SetFullScreen, fix rebase issues * Add warning to fragment_timeline* files --- changelog.d/7436.feature | 1 + .../src/main/res/values/strings.xml | 1 + vector/src/main/AndroidManifest.xml | 3 +- .../app/core/extensions/ViewExtensions.kt | 21 ++ .../JumpToBottomViewVisibilityManager.kt | 10 +- .../home/room/detail/TimelineFragment.kt | 52 +++- .../detail/composer/MessageComposerAction.kt | 2 + .../composer/MessageComposerFragment.kt | 22 +- .../detail/composer/MessageComposerView.kt | 12 +- .../composer/MessageComposerViewModel.kt | 15 +- .../composer/MessageComposerViewState.kt | 1 + .../composer/PlainTextComposerLayout.kt | 12 +- .../detail/composer/RichTextComposerLayout.kt | 47 ++-- .../res/drawable/ic_composer_full_screen.xml | 9 + .../res/layout/composer_rich_text_layout.xml | 14 +- ...ich_text_layout_constraint_set_compact.xml | 21 +- ...ch_text_layout_constraint_set_expanded.xml | 18 +- ..._text_layout_constraint_set_fullscreen.xml | 217 +++++++++++++++ .../src/main/res/layout/fragment_composer.xml | 4 +- .../src/main/res/layout/fragment_timeline.xml | 18 ++ .../layout/fragment_timeline_fullscreen.xml | 258 ++++++++++++++++++ 21 files changed, 705 insertions(+), 53 deletions(-) create mode 100644 changelog.d/7436.feature create mode 100644 vector/src/main/res/drawable/ic_composer_full_screen.xml create mode 100644 vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml create mode 100644 vector/src/main/res/layout/fragment_timeline_fullscreen.xml diff --git a/changelog.d/7436.feature b/changelog.d/7436.feature new file mode 100644 index 0000000000..b038c975e1 --- /dev/null +++ b/changelog.d/7436.feature @@ -0,0 +1 @@ +Rich text editor: add full screen mode. diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index ea9b4b5999..450dcab1f7 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3423,5 +3423,6 @@ Apply italic format Apply strikethrough format Apply underline format + Toggle full screen mode diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index b0cd202d12..11a54e9f82 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -150,7 +150,8 @@ + android:parentActivityName=".features.home.HomeActivity" + android:windowSoftInputMode="adjustResize"> diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt index 625ff15ef7..156809d5ad 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewExtensions.kt @@ -29,7 +29,13 @@ import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible +import androidx.transition.ChangeBounds +import androidx.transition.Fade +import androidx.transition.Transition +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet import im.vector.app.R +import im.vector.app.core.animations.SimpleTransitionListener import im.vector.app.features.themes.ThemeUtils /** @@ -90,3 +96,18 @@ fun View.setAttributeBackground(@AttrRes attributeId: Int) { val attribute = ThemeUtils.getAttribute(context, attributeId)!! setBackgroundResource(attribute.resourceId) } + +fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) { + val transition = TransitionSet().apply { + ordering = TransitionSet.ORDERING_SEQUENTIAL + addTransition(ChangeBounds()) + addTransition(Fade(Fade.IN)) + duration = animationDuration + addListener(object : SimpleTransitionListener() { + override fun onTransitionEnd(transition: Transition) { + transitionComplete?.invoke() + } + }) + } + TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt index 0f7dc251ae..1368b71ec6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -34,6 +34,8 @@ class JumpToBottomViewVisibilityManager( private val layoutManager: LinearLayoutManager ) { + private var canShowButtonOnScroll = true + init { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -43,7 +45,7 @@ class JumpToBottomViewVisibilityManager( if (scrollingToPast) { jumpToBottomView.hide() - } else { + } else if (canShowButtonOnScroll) { maybeShowJumpToBottomViewVisibility() } } @@ -66,7 +68,13 @@ class JumpToBottomViewVisibilityManager( } } + fun hideAndPreventVisibilityChangesWithScrolling() { + jumpToBottomView.hide() + canShowButtonOnScroll = false + } + private fun maybeShowJumpToBottomViewVisibility() { + canShowButtonOnScroll = true if (layoutManager.findFirstVisibleItemPosition() > 1) { jumpToBottomView.show() } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 9d50cdb070..4f51922a62 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -32,7 +32,9 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.activity.addCallback import androidx.appcompat.view.menu.MenuBuilder +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.net.toUri @@ -64,6 +66,7 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelperFactory import im.vector.app.core.epoxy.LayoutManagerStateRestorer +import im.vector.app.core.extensions.animateLayoutChange import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.containsRtLOverride @@ -183,7 +186,9 @@ import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -337,6 +342,7 @@ class TimelineFragment : setupJumpToBottomView() setupRemoveJitsiWidgetView() setupLiveLocationIndicator() + setupBackPressHandling() views.includeRoomToolbar.roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) @@ -414,6 +420,31 @@ class TimelineFragment : if (savedInstanceState == null) { handleSpaceShare() } + + views.scrim.setOnClickListener { + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false)) + } + + messageComposerViewModel.stateFlow.map { it.isFullScreen } + .distinctUntilChanged() + .onEach { isFullScreen -> + toggleFullScreenEditor(isFullScreen) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun setupBackPressHandling() { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + withState(messageComposerViewModel) { state -> + if (state.isFullScreen) { + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false)) + } else { + remove() // Remove callback to avoid infinite loop + @Suppress("DEPRECATION") + requireActivity().onBackPressed() + } + } + } } private fun setupRemoveJitsiWidgetView() { @@ -1016,7 +1047,13 @@ class TimelineFragment : override fun onLayoutCompleted(state: RecyclerView.State) { super.onLayoutCompleted(state) updateJumpToReadMarkerViewVisibility() - jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() + withState(messageComposerViewModel) { composerState -> + if (!composerState.isFullScreen) { + jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() + } else { + jumpToBottomViewVisibilityManager.hideAndPreventVisibilityChangesWithScrolling() + } + } } }.apply { // For local rooms, pin the view's content to the top edge (the layout is reversed) @@ -2002,6 +2039,19 @@ class TimelineFragment : } } + private fun toggleFullScreenEditor(isFullScreen: Boolean) { + views.composerContainer.animateLayoutChange(200) + + val constraintSet = ConstraintSet() + val constraintSetId = if (isFullScreen) { + R.layout.fragment_timeline_fullscreen + } else { + R.layout.fragment_timeline + } + constraintSet.clone(requireContext(), constraintSetId) + constraintSet.applyTo(views.rootConstraintLayout) + } + /** * Returns true if the current room is a Thread room, false otherwise. */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt index 82adcd014a..30437a016d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt @@ -34,6 +34,8 @@ sealed class MessageComposerAction : VectorViewModelAction { data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction() data class InsertUserDisplayName(val userId: String) : MessageComposerAction() + data class SetFullScreen(val isFullScreen: Boolean) : MessageComposerAction() + // Voice Message data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction() data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 55ec922a57..beb7215c22 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -92,6 +92,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.share.SharedData import im.vector.app.features.voice.VoiceFailure import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -219,6 +220,13 @@ class MessageComposerFragment : VectorBaseFragment(), A } } + messageComposerViewModel.stateFlow.map { it.isFullScreen } + .distinctUntilChanged() + .onEach { isFullScreen -> + composer.toggleFullScreen(isFullScreen) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + if (savedInstanceState != null) { handleShareData() } @@ -297,7 +305,7 @@ class MessageComposerFragment : VectorBaseFragment(), A // Show keyboard when the user started a thread composerEditText.showKeyboard(andRequestFocus = true) } - composer.callback = object : PlainTextComposerLayout.Callback { + composer.callback = object : Callback { override fun onAddAttachment() { if (!::attachmentTypeSelector.isInitialized) { attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment) @@ -320,8 +328,12 @@ class MessageComposerFragment : VectorBaseFragment(), A composer.emojiButton?.isVisible = isEmojiKeyboardVisible } - override fun onSendMessage(text: CharSequence) { + override fun onSendMessage(text: CharSequence) = withState(messageComposerViewModel) { state -> sendTextMessage(text, composer.formattedText) + + if (state.isFullScreen) { + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false)) + } } override fun onCloseRelatedMessage() { @@ -335,6 +347,10 @@ class MessageComposerFragment : VectorBaseFragment(), A override fun onTextChanged(text: CharSequence) { messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text)) } + + override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state -> + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen)) + } } } @@ -461,7 +477,7 @@ class MessageComposerFragment : VectorBaseFragment(), A composer.sendButton.alpha = 0f composer.sendButton.isVisible = true composer.sendButton.animate().alpha(1f).setDuration(150).start() - } else { + } else if (!event.isVisible) { composer.sendButton.isInvisible = true } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt index 09357191b4..b7e0e29679 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt @@ -30,13 +30,14 @@ interface MessageComposerView { val emojiButton: ImageButton? val sendButton: ImageButton val attachmentButton: ImageButton + val fullScreenButton: ImageButton? val composerRelatedMessageTitle: TextView val composerRelatedMessageContent: TextView val composerRelatedMessageImage: ImageView val composerRelatedMessageActionIcon: ImageView val composerRelatedMessageAvatar: ImageView - var callback: PlainTextComposerLayout.Callback? + var callback: Callback? var isVisible: Boolean @@ -44,6 +45,15 @@ interface MessageComposerView { fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) fun setTextIfDifferent(text: CharSequence?): Boolean fun replaceFormattedContent(text: CharSequence) + fun toggleFullScreen(newValue: Boolean) fun setInvisible(isInvisible: Boolean) } + +interface Callback : ComposerEditText.Callback { + fun onCloseRelatedMessage() + fun onSendMessage(text: CharSequence) + fun onAddAttachment() + fun onExpandOrCompactChange() + fun onFullScreenModeChanged() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 1a9f9e6291..23d6e71114 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.composer +import android.text.SpannableString import androidx.lifecycle.asFlow import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted @@ -122,6 +123,7 @@ class MessageComposerViewModel @AssistedInject constructor( is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action) + is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action) } } @@ -130,12 +132,11 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) { - setState { - // Makes sure currentComposerText is upToDate when accessing further setState - currentComposerText = action.text - this + val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty() + currentComposerText = SpannableString(action.text) + if (needsSendButtonVisibilityUpdate) { + updateIsSendButtonVisibility(true) } - updateIsSendButtonVisibility(true) } private fun subscribeToStateInternal() { @@ -163,6 +164,10 @@ class MessageComposerViewModel @AssistedInject constructor( } } + private fun handleSetFullScreen(action: MessageComposerAction.SetFullScreen) { + setState { copy(isFullScreen = action.isFullScreen) } + } + private fun observePowerLevelAndEncryption() { combine( PowerLevelsFlowFactory(room).createFlow(), diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 0df1dbebd8..7bb9509599 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -70,6 +70,7 @@ data class MessageComposerViewState( val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle, val voiceBroadcastState: VoiceBroadcastState? = null, val text: CharSequence? = null, + val isFullScreen: Boolean = false, ) : MavericksState { val isVoiceRecording = when (voiceRecordingUiState) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt index acb5a1b42a..939a59fcca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt @@ -49,13 +49,6 @@ class PlainTextComposerLayout @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView { - interface Callback : ComposerEditText.Callback { - fun onCloseRelatedMessage() - fun onSendMessage(text: CharSequence) - fun onAddAttachment() - fun onExpandOrCompactChange() - } - private val views: ComposerLayoutBinding override var callback: Callback? = null @@ -83,6 +76,7 @@ class PlainTextComposerLayout @JvmOverloads constructor( } override val attachmentButton: ImageButton get() = views.attachmentButton + override val fullScreenButton: ImageButton? = null override val composerRelatedMessageActionIcon: ImageView get() = views.composerRelatedMessageActionIcon override val composerRelatedMessageAvatar: ImageView @@ -155,6 +149,10 @@ class PlainTextComposerLayout @JvmOverloads constructor( return views.composerEditText.setTextIfDifferent(text) } + override fun toggleFullScreen(newValue: Boolean) { + // Plain text composer has no full screen + } + private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { // val wasSendButtonInvisible = views.sendButton.isInvisible if (animate) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 07b7d151ad..cac8f8bed4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -21,7 +21,6 @@ import android.text.Editable import android.text.TextWatcher import android.util.AttributeSet import android.view.LayoutInflater -import android.view.ViewGroup import android.widget.EditText import android.widget.ImageButton import android.widget.ImageView @@ -33,13 +32,8 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.transition.ChangeBounds -import androidx.transition.Fade -import androidx.transition.Transition -import androidx.transition.TransitionManager -import androidx.transition.TransitionSet import im.vector.app.R -import im.vector.app.core.animations.SimpleTransitionListener +import im.vector.app.core.extensions.animateLayoutChange import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding @@ -56,12 +50,13 @@ class RichTextComposerLayout @JvmOverloads constructor( private val views: ComposerRichTextLayoutBinding - override var callback: PlainTextComposerLayout.Callback? = null + override var callback: Callback? = null private var currentConstraintSetId: Int = -1 - private val animationDuration = 100L + private var isFullScreen = false + override val text: Editable? get() = views.composerEditText.text override val formattedText: String? @@ -74,6 +69,8 @@ class RichTextComposerLayout @JvmOverloads constructor( get() = views.sendButton override val attachmentButton: ImageButton get() = views.attachmentButton + override val fullScreenButton: ImageButton? + get() = views.composerFullScreenButton override val composerRelatedMessageActionIcon: ImageView get() = views.composerRelatedMessageActionIcon override val composerRelatedMessageAvatar: ImageView @@ -124,6 +121,10 @@ class RichTextComposerLayout @JvmOverloads constructor( callback?.onAddAttachment() } + views.composerFullScreenButton.setOnClickListener { + callback?.onFullScreenModeChanged() + } + setupRichTextMenu() } @@ -205,34 +206,30 @@ class RichTextComposerLayout @JvmOverloads constructor( return views.composerEditText.setTextIfDifferent(text) } + override fun toggleFullScreen(newValue: Boolean) { + val constraintSetId = if (newValue) R.layout.composer_rich_text_layout_constraint_set_fullscreen else currentConstraintSetId + ConstraintSet().also { + it.clone(context, constraintSetId) + it.applyTo(this) + } + + updateTextFieldBorder(newValue) + } + private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { // val wasSendButtonInvisible = views.sendButton.isInvisible if (animate) { - configureAndBeginTransition(transitionComplete) + animateLayoutChange(animationDuration, transitionComplete) } ConstraintSet().also { it.clone(context, currentConstraintSetId) it.applyTo(this) } + // Might be updated by view state just after, but avoid blinks // views.sendButton.isInvisible = wasSendButtonInvisible } - private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) { - val transition = TransitionSet().apply { - ordering = TransitionSet.ORDERING_SEQUENTIAL - addTransition(ChangeBounds()) - addTransition(Fade(Fade.IN)) - duration = animationDuration - addListener(object : SimpleTransitionListener() { - override fun onTransitionEnd(transition: Transition) { - transitionComplete?.invoke() - } - }) - } - TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition) - } - override fun setInvisible(isInvisible: Boolean) { this.isInvisible = isInvisible } diff --git a/vector/src/main/res/drawable/ic_composer_full_screen.xml b/vector/src/main/res/drawable/ic_composer_full_screen.xml new file mode 100644 index 0000000000..394dc52279 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_full_screen.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index 09e4b03887..9f49b8f9d6 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" tools:constraintSet="@layout/composer_rich_text_layout_constraint_set_compact" tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> @@ -108,12 +108,24 @@ style="@style/Widget.Vector.EditText.RichTextComposer" android:layout_width="0dp" android:layout_height="wrap_content" + android:gravity="top" android:nextFocusLeft="@id/composerEditText" android:nextFocusUp="@id/composerEditText" tools:hint="@string/room_message_placeholder" tools:text="@tools:sample/lorem/random" tools:ignore="MissingConstraints" /> + + @@ -114,6 +114,7 @@ android:background="?android:attr/selectableItemBackground" android:contentDescription="@string/option_send_files" android:src="@drawable/ic_attachment" + app:layout_constraintVertical_bias="1" app:layout_constraintBottom_toBottomOf="@id/sendButton" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/sendButton" @@ -142,14 +143,26 @@ android:hint="@string/room_message_placeholder" android:nextFocusLeft="@id/composerEditText" android:nextFocusUp="@id/composerEditText" - android:layout_marginHorizontal="12dp" + android:layout_marginStart="12dp" android:layout_marginVertical="10dp" app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder" - app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" + app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton" app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder" tools:text="@tools:sample/lorem/random" /> + + @@ -173,6 +187,7 @@ app:layout_constraintStart_toEndOf="@id/attachmentButton" app:layout_constraintEnd_toStartOf="@id/sendButton" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="1" android:fillViewport="true"> @@ -156,14 +156,26 @@ android:hint="@string/room_message_placeholder" android:nextFocusLeft="@id/composerEditText" android:nextFocusUp="@id/composerEditText" - android:layout_marginHorizontal="12dp" + android:layout_marginStart="12dp" android:layout_marginVertical="10dp" app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder" - app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" + app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton" app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder" tools:text="@tools:sample/lorem/random" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_composer.xml b/vector/src/main/res/layout/fragment_composer.xml index 8703af7471..41c052367a 100644 --- a/vector/src/main/res/layout/fragment_composer.xml +++ b/vector/src/main/res/layout/fragment_composer.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> + + + + @@ -165,6 +182,7 @@ android:layout_margin="16dp" android:contentDescription="@string/a11y_jump_to_bottom" android:src="@drawable/ic_expand_more" + android:visibility="gone" app:backgroundTint="#FFFFFF" app:badgeBackgroundColor="?colorPrimary" app:badgeTextColor="?colorOnPrimary" diff --git a/vector/src/main/res/layout/fragment_timeline_fullscreen.xml b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml new file mode 100644 index 0000000000..373ca74f56 --- /dev/null +++ b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c20f6fe3262761b918cbc5c686335884bea6e57f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:07:38 +0200 Subject: [PATCH 026/679] GetOngoingVoiceBroadcastsUseCase: Remove debug logs --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index cb228ad8aa..0f5e413719 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -31,12 +31,8 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor( ) { fun execute(roomId: String): List { - println("## GetOngoingVoiceBroadcastsUseCase") - println("## GetOngoingVoiceBroadcastsUseCase activeSessionHolder $activeSessionHolder") val session = activeSessionHolder.getSafeActiveSession() - println("## GetOngoingVoiceBroadcastsUseCase session $session") val room = session?.getRoom(roomId) ?: error("Unknown roomId: $roomId") - println("## GetOngoingVoiceBroadcastsUseCase room $room") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") From cb5fc75c5d19751dba201b41f52cccde778893d0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:08:03 +0200 Subject: [PATCH 027/679] GetOngoingVoiceBroadcastsUseCase: Return empty list if there is no session --- .../usecase/GetOngoingVoiceBroadcastsUseCase.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt index 0f5e413719..ec50618969 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -31,8 +31,11 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor( ) { fun execute(roomId: String): List { - val session = activeSessionHolder.getSafeActiveSession() - val room = session?.getRoom(roomId) ?: error("Unknown roomId: $roomId") + val session = activeSessionHolder.getSafeActiveSession() ?: run { + Timber.d("## GetOngoingVoiceBroadcastsUseCase: no active session") + return emptyList() + } + val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") From bdfc96ff666859b11376e91635c458dc242412ca Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:36:02 +0200 Subject: [PATCH 028/679] Fix merge conflicts --- .../detail/composer/MessageComposerFragment.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 3cb3eb1a4b..463a8fe440 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -91,8 +91,8 @@ import im.vector.app.features.poll.PollMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.share.SharedData import im.vector.app.features.voice.VoiceFailure -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -207,6 +207,13 @@ class MessageComposerFragment : VectorBaseFragment(), A } } + messageComposerViewModel.stateFlow.map { it.isFullScreen } + .distinctUntilChanged() + .onEach { isFullScreen -> + composer.toggleFullScreen(isFullScreen) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend -> if (!canSend.boolean()) { return@onEach @@ -220,13 +227,6 @@ class MessageComposerFragment : VectorBaseFragment(), A } } - messageComposerViewModel.stateFlow.map { it.isFullScreen } - .distinctUntilChanged() - .onEach { isFullScreen -> - composer.toggleFullScreen(isFullScreen) - } - .launchIn(viewLifecycleOwner.lifecycleScope) - if (savedInstanceState != null) { handleShareData() } From c776aae9d06052abe2eb0d799ac33cc77dc94e9f Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Wed, 26 Oct 2022 17:37:40 +0100 Subject: [PATCH 029/679] [Rich text editor] Add plain text mode and new attachment UI (#7459) * Add new attachments selection dialog * Add rounded corners to bottom sheet dialog. Note these are currently only visible in the collapsed state. - [Google issue](https://issuetracker.google.com/issues/144859239) - [Rejected PR](https://github.com/material-components/material-components-android/pull/437) - [Github issue](https://github.com/material-components/material-components-android/issues/1278) * Add changelog entry * Remove redundant call to superclass click listener * Refactor to use view visibility helper * Change redundant sealed class to interface * Remove unused string * Revert "Add rounded corners to bottom sheet dialog." This reverts commit 17c43c91888162d3c7675511ff910c46c3aa32fc. * Remove redundant view group * Remove redundant `this` * Update rich text editor to latest * Update rich text editor version * Allow toggling rich text in the new editor * Persist the text formatting setting * Add changelog entry --- changelog.d/7429.feature | 1 + changelog.d/7452.feature | 1 + dependencies.gradle | 2 +- .../src/main/res/values/strings.xml | 10 ++ .../app/core/di/MavericksViewModelModule.kt | 6 + .../core/ui/views/BottomSheetActionButton.kt | 4 + .../features/attachments/AttachmentType.kt | 37 +++++ .../AttachmentTypeSelectorBottomSheet.kt | 92 ++++++++++++ ...chmentTypeSelectorSharedActionViewModel.kt | 30 ++++ .../attachments/AttachmentTypeSelectorView.kt | 70 +++++---- .../AttachmentTypeSelectorViewModel.kt | 76 ++++++++++ .../features/attachments/AttachmentsHelper.kt | 2 +- .../composer/MessageComposerFragment.kt | 74 +++++---- .../detail/composer/RichTextComposerLayout.kt | 99 ++++++++---- .../features/settings/VectorPreferences.kt | 19 +++ .../main/res/drawable/ic_text_formatting.xml | 13 ++ .../drawable/ic_text_formatting_disabled.xml | 18 +++ .../bottom_sheet_attachment_type_selector.xml | 106 +++++++++++++ .../res/layout/composer_rich_text_layout.xml | 19 ++- ...ich_text_layout_constraint_set_compact.xml | 22 ++- ...ch_text_layout_constraint_set_expanded.xml | 22 ++- ..._text_layout_constraint_set_fullscreen.xml | 23 ++- .../AttachmentTypeSelectorViewModelTest.kt | 142 ++++++++++++++++++ .../app/test/fakes/FakeVectorFeatures.kt | 8 + .../app/test/fakes/FakeVectorPreferences.kt | 3 + 25 files changed, 797 insertions(+), 102 deletions(-) create mode 100644 changelog.d/7429.feature create mode 100644 changelog.d/7452.feature create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt create mode 100644 vector/src/main/res/drawable/ic_text_formatting.xml create mode 100644 vector/src/main/res/drawable/ic_text_formatting_disabled.xml create mode 100644 vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml create mode 100644 vector/src/test/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModelTest.kt diff --git a/changelog.d/7429.feature b/changelog.d/7429.feature new file mode 100644 index 0000000000..9857452eca --- /dev/null +++ b/changelog.d/7429.feature @@ -0,0 +1 @@ +Add new UI for selecting an attachment diff --git a/changelog.d/7452.feature b/changelog.d/7452.feature new file mode 100644 index 0000000000..a811f87c84 --- /dev/null +++ b/changelog.d/7452.feature @@ -0,0 +1 @@ +[Rich text editor] Add plain text mode diff --git a/dependencies.gradle b/dependencies.gradle index f081e0a874..db6e92552a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.2.1" + 'wysiwyg' : "io.element.android:wysiwyg:0.4.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 450dcab1f7..9edd7d836a 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3205,6 +3205,16 @@ Share location Start a voice broadcast + Photo library + Stickers + Attachments + Voice broadcast + Polls + Location + Camera + Contact + Text formatting + Show less "%1$d more" diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 97590028d8..2242abb7aa 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -22,6 +22,7 @@ import dagger.hilt.InstallIn import dagger.multibindings.IntoMap import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel +import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel import im.vector.app.features.auth.ReAuthViewModel import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel @@ -677,4 +678,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(VectorSettingsLabsViewModel::class) fun vectorSettingsLabsViewModelFactory(factory: VectorSettingsLabsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class) + fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt index a3e8b3780c..ca3e6a360a 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt @@ -38,6 +38,10 @@ class BottomSheetActionButton @JvmOverloads constructor( ) : FrameLayout(context, attrs, defStyleAttr) { val views: ViewBottomSheetActionButtonBinding + override fun setOnClickListener(l: OnClickListener?) { + views.bottomSheetActionClickableZone.setOnClickListener(l) + } + var title: String? = null set(value) { field = value diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt new file mode 100644 index 0000000000..f4b97b9f9c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentType.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.attachments + +import im.vector.app.core.utils.PERMISSIONS_EMPTY +import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING +import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST + +/** + * The all possible types to pick with their required permissions. + */ +enum class AttachmentType(val permissions: List) { + CAMERA(PERMISSIONS_FOR_TAKING_PHOTO), + GALLERY(PERMISSIONS_EMPTY), + FILE(PERMISSIONS_EMPTY), + STICKER(PERMISSIONS_EMPTY), + CONTACT(PERMISSIONS_FOR_PICKING_CONTACT), + POLL(PERMISSIONS_EMPTY), + LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING), + VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST), +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt new file mode 100644 index 0000000000..f8d5d768ef --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorBottomSheet.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.attachments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetAttachmentTypeSelectorBinding +import im.vector.app.features.home.room.detail.TimelineViewModel + +@AndroidEntryPoint +class AttachmentTypeSelectorBottomSheet : VectorBaseBottomSheetDialogFragment() { + + private val viewModel: AttachmentTypeSelectorViewModel by parentFragmentViewModel() + private val timelineViewModel: TimelineViewModel by parentFragmentViewModel() + private val sharedActionViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels( + ownerProducer = { requireParentFragment() } + ) + + override val showExpanded = true + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetAttachmentTypeSelectorBinding { + return BottomSheetAttachmentTypeSelectorBinding.inflate(inflater, container, false) + } + + override fun invalidate() = withState(viewModel, timelineViewModel) { viewState, timelineState -> + super.invalidate() + views.location.isVisible = viewState.isLocationVisible + views.voiceBroadcast.isVisible = viewState.isVoiceBroadcastVisible + views.poll.isVisible = !timelineState.isThreadTimeline() + views.textFormatting.isChecked = viewState.isTextFormattingEnabled + views.textFormatting.setCompoundDrawablesRelativeWithIntrinsicBounds( + if (viewState.isTextFormattingEnabled) { + R.drawable.ic_text_formatting + } else { + R.drawable.ic_text_formatting_disabled + }, 0, 0, 0 + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.gallery.debouncedClicks { onAttachmentSelected(AttachmentType.GALLERY) } + views.stickers.debouncedClicks { onAttachmentSelected(AttachmentType.STICKER) } + views.file.debouncedClicks { onAttachmentSelected(AttachmentType.FILE) } + views.voiceBroadcast.debouncedClicks { onAttachmentSelected(AttachmentType.VOICE_BROADCAST) } + views.poll.debouncedClicks { onAttachmentSelected(AttachmentType.POLL) } + views.location.debouncedClicks { onAttachmentSelected(AttachmentType.LOCATION) } + views.camera.debouncedClicks { onAttachmentSelected(AttachmentType.CAMERA) } + views.contact.debouncedClicks { onAttachmentSelected(AttachmentType.CONTACT) } + views.textFormatting.setOnCheckedChangeListener { _, isChecked -> onTextFormattingToggled(isChecked) } + } + + private fun onAttachmentSelected(attachmentType: AttachmentType) { + val action = AttachmentTypeSelectorSharedAction.SelectAttachmentTypeAction(attachmentType) + sharedActionViewModel.post(action) + dismiss() + } + + private fun onTextFormattingToggled(isEnabled: Boolean) = + viewModel.handle(AttachmentTypeSelectorAction.ToggleTextFormatting(isEnabled)) + + companion object { + fun show(fragmentManager: FragmentManager) { + val bottomSheet = AttachmentTypeSelectorBottomSheet() + bottomSheet.show(fragmentManager, "AttachmentTypeSelectorBottomSheet") + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt new file mode 100644 index 0000000000..e02b10c54b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorSharedActionViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package im.vector.app.features.attachments + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class AttachmentTypeSelectorSharedActionViewModel @Inject constructor() : + VectorSharedActionViewModel() + +sealed interface AttachmentTypeSelectorSharedAction : VectorSharedAction { + data class SelectAttachmentTypeAction( + val attachmentType: AttachmentType + ) : AttachmentTypeSelectorSharedAction +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index 8536b765d4..55805a0728 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -30,17 +30,11 @@ import android.view.animation.TranslateAnimation import android.widget.ImageButton import android.widget.LinearLayout import android.widget.PopupWindow -import androidx.annotation.StringRes import androidx.appcompat.widget.TooltipCompat import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.epoxy.onClick -import im.vector.app.core.utils.PERMISSIONS_EMPTY -import im.vector.app.core.utils.PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING -import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT -import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.app.core.utils.PERMISSIONS_FOR_VOICE_BROADCAST import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback import kotlin.math.max @@ -59,7 +53,7 @@ class AttachmentTypeSelectorView( ) : PopupWindow(context) { interface Callback { - fun onTypeSelected(type: Type) + fun onTypeSelected(type: AttachmentType) } private val views: ViewAttachmentTypeSelectorBinding @@ -69,14 +63,14 @@ class AttachmentTypeSelectorView( init { contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false) views = ViewAttachmentTypeSelectorBinding.bind(contentView) - views.attachmentGalleryButton.configure(Type.GALLERY) - views.attachmentCameraButton.configure(Type.CAMERA) - views.attachmentFileButton.configure(Type.FILE) - views.attachmentStickersButton.configure(Type.STICKER) - views.attachmentContactButton.configure(Type.CONTACT) - views.attachmentPollButton.configure(Type.POLL) - views.attachmentLocationButton.configure(Type.LOCATION) - views.attachmentVoiceBroadcast.configure(Type.VOICE_BROADCAST) + views.attachmentGalleryButton.configure(AttachmentType.GALLERY) + views.attachmentCameraButton.configure(AttachmentType.CAMERA) + views.attachmentFileButton.configure(AttachmentType.FILE) + views.attachmentStickersButton.configure(AttachmentType.STICKER) + views.attachmentContactButton.configure(AttachmentType.CONTACT) + views.attachmentPollButton.configure(AttachmentType.POLL) + views.attachmentLocationButton.configure(AttachmentType.LOCATION) + views.attachmentVoiceBroadcast.configure(AttachmentType.VOICE_BROADCAST) width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT animationStyle = 0 @@ -127,16 +121,16 @@ class AttachmentTypeSelectorView( } } - fun setAttachmentVisibility(type: Type, isVisible: Boolean) { + fun setAttachmentVisibility(type: AttachmentType, isVisible: Boolean) { when (type) { - Type.CAMERA -> views.attachmentCameraButton - Type.GALLERY -> views.attachmentGalleryButton - Type.FILE -> views.attachmentFileButton - Type.STICKER -> views.attachmentStickersButton - Type.CONTACT -> views.attachmentContactButton - Type.POLL -> views.attachmentPollButton - Type.LOCATION -> views.attachmentLocationButton - Type.VOICE_BROADCAST -> views.attachmentVoiceBroadcast + AttachmentType.CAMERA -> views.attachmentCameraButton + AttachmentType.GALLERY -> views.attachmentGalleryButton + AttachmentType.FILE -> views.attachmentFileButton + AttachmentType.STICKER -> views.attachmentStickersButton + AttachmentType.CONTACT -> views.attachmentContactButton + AttachmentType.POLL -> views.attachmentPollButton + AttachmentType.LOCATION -> views.attachmentLocationButton + AttachmentType.VOICE_BROADCAST -> views.attachmentVoiceBroadcast }.let { it.isVisible = isVisible } @@ -200,13 +194,13 @@ class AttachmentTypeSelectorView( return Pair(x, y) } - private fun ImageButton.configure(type: Type): ImageButton { + private fun ImageButton.configure(type: AttachmentType): ImageButton { this.setOnClickListener(TypeClickListener(type)) - TooltipCompat.setTooltipText(this, context.getString(type.tooltipRes)) + TooltipCompat.setTooltipText(this, context.getString(attachmentTooltipLabels.getValue(type))) return this } - private inner class TypeClickListener(private val type: Type) : View.OnClickListener { + private inner class TypeClickListener(private val type: AttachmentType) : View.OnClickListener { override fun onClick(v: View) { dismiss() @@ -217,14 +211,18 @@ class AttachmentTypeSelectorView( /** * The all possible types to pick with their required permissions and tooltip resource. */ - enum class Type(val permissions: List, @StringRes val tooltipRes: Int) { - CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo), - GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery), - FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file), - STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), - CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), - POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), - LOCATION(PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING, R.string.tooltip_attachment_location), - VOICE_BROADCAST(PERMISSIONS_FOR_VOICE_BROADCAST, R.string.tooltip_attachment_voice_broadcast), + private companion object { + private val attachmentTooltipLabels: Map = AttachmentType.values().associateWith { + when (it) { + AttachmentType.CAMERA -> R.string.tooltip_attachment_photo + AttachmentType.GALLERY -> R.string.tooltip_attachment_gallery + AttachmentType.FILE -> R.string.tooltip_attachment_file + AttachmentType.STICKER -> R.string.tooltip_attachment_sticker + AttachmentType.CONTACT -> R.string.tooltip_attachment_contact + AttachmentType.POLL -> R.string.tooltip_attachment_poll + AttachmentType.LOCATION -> R.string.tooltip_attachment_location + AttachmentType.VOICE_BROADCAST -> R.string.tooltip_attachment_voice_broadcast + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt new file mode 100644 index 0000000000..cb74661eba --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorViewModel.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.attachments + +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.VectorFeatures +import im.vector.app.features.settings.VectorPreferences + +class AttachmentTypeSelectorViewModel @AssistedInject constructor( + @Assisted initialState: AttachmentTypeSelectorViewState, + private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, +) : VectorViewModel(initialState) { + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: AttachmentTypeSelectorViewState): AttachmentTypeSelectorViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: AttachmentTypeSelectorAction) = when (action) { + is AttachmentTypeSelectorAction.ToggleTextFormatting -> setTextFormattingEnabled(action.isEnabled) + } + + init { + setState { + copy( + isLocationVisible = vectorFeatures.isLocationSharingEnabled(), + isVoiceBroadcastVisible = vectorFeatures.isVoiceBroadcastEnabled(), + isTextFormattingEnabled = vectorPreferences.isTextFormattingEnabled(), + ) + } + } + + private fun setTextFormattingEnabled(isEnabled: Boolean) { + vectorPreferences.setTextFormattingEnabled(isEnabled) + setState { + copy( + isTextFormattingEnabled = isEnabled + ) + } + } +} + +data class AttachmentTypeSelectorViewState( + val isLocationVisible: Boolean = false, + val isVoiceBroadcastVisible: Boolean = false, + val isTextFormattingEnabled: Boolean = false, +) : MavericksState + +sealed interface AttachmentTypeSelectorAction : VectorViewModelAction { + data class ToggleTextFormatting(val isEnabled: Boolean) : AttachmentTypeSelectorAction +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index 1a8e10d102..9692777e15 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -54,7 +54,7 @@ class AttachmentsHelper( private var captureUri: Uri? = null // The pending type is set if we have to handle permission request. It must be restored if the activity gets killed. - var pendingType: AttachmentTypeSelectorView.Type? = null + var pendingType: AttachmentType? = null // Restorable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 463a8fe440..5666c28605 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -40,8 +40,10 @@ import androidx.core.text.buildSpannedString import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -63,7 +65,12 @@ import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentComposerBinding import im.vector.app.features.VectorFeatures +import im.vector.app.features.attachments.AttachmentType +import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet +import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction +import im.vector.app.features.attachments.AttachmentTypeSelectorSharedActionViewModel import im.vector.app.features.attachments.AttachmentTypeSelectorView +import im.vector.app.features.attachments.AttachmentTypeSelectorViewModel import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.ShareIntentHandler @@ -91,8 +98,9 @@ import im.vector.app.features.poll.PollMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.share.SharedData import im.vector.app.features.voice.VoiceFailure -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -162,6 +170,8 @@ class MessageComposerFragment : VectorBaseFragment(), A private val timelineViewModel: TimelineViewModel by parentFragmentViewModel() private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel() private lateinit var sharedActionViewModel: MessageSharedActionViewModel + private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel() + private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels() private val composer: MessageComposerView get() { return if (vectorPreferences.isRichTextEditorEnabled()) { @@ -227,6 +237,11 @@ class MessageComposerFragment : VectorBaseFragment(), A } } + attachmentActionsViewModel.stream() + .filterIsInstance() + .onEach { onTypeSelected(it.attachmentType) } + .launchIn(lifecycleScope) + if (savedInstanceState != null) { handleShareData() } @@ -260,11 +275,14 @@ class MessageComposerFragment : VectorBaseFragment(), A messageComposerViewModel.endAllVoiceActions() } - override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState -> + override fun invalidate() = withState( + timelineViewModel, messageComposerViewModel, attachmentViewModel + ) { mainState, messageComposerState, attachmentState -> if (mainState.tombstoneEvent != null) return@withState composer.setInvisible(!messageComposerState.isComposerVisible) composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible + (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled } private fun setupComposer() { @@ -307,21 +325,25 @@ class MessageComposerFragment : VectorBaseFragment(), A } composer.callback = object : Callback { override fun onAddAttachment() { - if (!::attachmentTypeSelector.isInitialized) { - attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment) - attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.LOCATION, - vectorFeatures.isLocationSharingEnabled(), - ) - attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine() - ) - attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.VOICE_BROADCAST, - vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission - ) + if (vectorPreferences.isRichTextEditorEnabled()) { + AttachmentTypeSelectorBottomSheet.show(childFragmentManager) + } else { + if (!::attachmentTypeSelector.isInitialized) { + attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentType.LOCATION, + vectorFeatures.isLocationSharingEnabled(), + ) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentType.POLL, !isThreadTimeLine() + ) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentType.VOICE_BROADCAST, + vectorPreferences.isVoiceBroadcastEnabled(), // TODO check user permission + ) + } + attachmentTypeSelector.show(composer.attachmentButton) } - attachmentTypeSelector.show(composer.attachmentButton) } override fun onExpandOrCompactChange() { @@ -678,20 +700,20 @@ class MessageComposerFragment : VectorBaseFragment(), A } } - private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { + private fun launchAttachmentProcess(type: AttachmentType) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( + AttachmentType.CAMERA -> attachmentsHelper.openCamera( activity = requireActivity(), vectorPreferences = vectorPreferences, cameraActivityResultLauncher = attachmentCameraActivityResultLauncher, cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher ) - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) - AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) - AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment) - AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE) - AttachmentTypeSelectorView.Type.LOCATION -> { + AttachmentType.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) + AttachmentType.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) + AttachmentType.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) + AttachmentType.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment) + AttachmentType.POLL -> navigator.openCreatePoll(requireContext(), roomId, null, PollMode.CREATE) + AttachmentType.LOCATION -> { navigator .openLocationSharing( context = requireContext(), @@ -701,11 +723,11 @@ class MessageComposerFragment : VectorBaseFragment(), A locationOwnerId = session.myUserId ) } - AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start) + AttachmentType.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start) } } - override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { + override fun onTypeSelected(type: AttachmentType) { if (checkPermissions(type.permissions, requireActivity(), typeSelectedActivityResultLauncher)) { launchAttachmentProcess(type) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index cac8f8bed4..2c09f351bb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -38,7 +38,7 @@ import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding import io.element.android.wysiwyg.EditorEditText -import io.element.android.wysiwyg.InlineFormat +import io.element.android.wysiwyg.inputhandlers.models.InlineFormat import uniffi.wysiwyg_composer.ComposerAction import uniffi.wysiwyg_composer.MenuState @@ -57,12 +57,24 @@ class RichTextComposerLayout @JvmOverloads constructor( private var isFullScreen = false + var isTextFormattingEnabled = true + set(value) { + if (field == value) return + syncEditTexts() + field = value + updateEditTextVisibility() + } + override val text: Editable? - get() = views.composerEditText.text + get() = editText.text override val formattedText: String? - get() = views.composerEditText.getHtmlOutput() + get() = (editText as? EditorEditText)?.getHtmlOutput() override val editText: EditText - get() = views.composerEditText + get() = if (isTextFormattingEnabled) { + views.richTextComposerEditText + } else { + views.plainTextComposerEditText + } override val emojiButton: ImageButton? get() = null override val sendButton: ImageButton @@ -91,21 +103,12 @@ class RichTextComposerLayout @JvmOverloads constructor( collapse(false) - views.composerEditText.addTextChangedListener(object : TextWatcher { - private var previousTextWasExpanded = false - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable) { - callback?.onTextChanged(s) - - val isExpanded = s.lines().count() > 1 - if (previousTextWasExpanded != isExpanded) { - updateTextFieldBorder(isExpanded) - } - previousTextWasExpanded = isExpanded - } - }) + views.richTextComposerEditText.addTextChangedListener( + TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + ) + views.plainTextComposerEditText.addTextChangedListener( + TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + ) views.composerRelatedMessageCloseButton.setOnClickListener { collapse() @@ -130,19 +133,23 @@ class RichTextComposerLayout @JvmOverloads constructor( private fun setupRichTextMenu() { addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) { - views.composerEditText.toggleInlineFormat(InlineFormat.Bold) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold) } addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) { - views.composerEditText.toggleInlineFormat(InlineFormat.Italic) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic) } addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) { - views.composerEditText.toggleInlineFormat(InlineFormat.Underline) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline) } addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) { - views.composerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() - views.composerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state -> + views.richTextComposerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state -> if (state is MenuState.Update) { updateMenuStateFor(ComposerAction.Bold, state) updateMenuStateFor(ComposerAction.Italic, state) @@ -150,8 +157,26 @@ class RichTextComposerLayout @JvmOverloads constructor( updateMenuStateFor(ComposerAction.StrikeThrough, state) } } + + updateEditTextVisibility() } + private fun updateEditTextVisibility() { + views.richTextComposerEditText.isVisible = isTextFormattingEnabled + views.richTextMenu.isVisible = isTextFormattingEnabled + views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled + } + + /** + * Updates the non-active input with the contents of the active input. + */ + private fun syncEditTexts() = + if (isTextFormattingEnabled) { + views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText()) + } else { + views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString()) + } + private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) { val inflater = LayoutInflater.from(context) val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true) @@ -181,7 +206,7 @@ class RichTextComposerLayout @JvmOverloads constructor( } override fun replaceFormattedContent(text: CharSequence) { - views.composerEditText.setHtml(text.toString()) + views.richTextComposerEditText.setHtml(text.toString()) } override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) { @@ -191,6 +216,7 @@ class RichTextComposerLayout @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) + updateEditTextVisibility() } override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) { @@ -200,10 +226,11 @@ class RichTextComposerLayout @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) + updateEditTextVisibility() } override fun setTextIfDifferent(text: CharSequence?): Boolean { - return views.composerEditText.setTextIfDifferent(text) + return editText.setTextIfDifferent(text) } override fun toggleFullScreen(newValue: Boolean) { @@ -214,6 +241,7 @@ class RichTextComposerLayout @JvmOverloads constructor( } updateTextFieldBorder(newValue) + updateEditTextVisibility() } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { @@ -233,4 +261,23 @@ class RichTextComposerLayout @JvmOverloads constructor( override fun setInvisible(isInvisible: Boolean) { this.isInvisible = isInvisible } + + private class TextChangeListener( + private val onTextChanged: (s: Editable) -> Unit, + private val onExpandedChanged: (isExpanded: Boolean) -> Unit, + ) : TextWatcher { + private var previousTextWasExpanded = false + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + onTextChanged.invoke(s) + + val isExpanded = s.lines().count() > 1 + if (previousTextWasExpanded != isExpanded) { + onExpandedChanged(isExpanded) + } + previousTextWasExpanded = isExpanded + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 2dc8b12160..9f40a7cede 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -109,6 +109,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY" private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY" private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY" + private const val SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY = "SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY" private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY" private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY" private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY" @@ -759,6 +760,24 @@ class VectorPreferences @Inject constructor( } } + /** + * Tells if text formatting is enabled within the rich text editor. + * + * @return true if the text formatting is enabled + */ + fun isTextFormattingEnabled(): Boolean = + defaultPrefs.getBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, true) + + /** + * Update whether text formatting is enabled within the rich text editor. + * + * @param isEnabled true to enable the text formatting + */ + fun setTextFormattingEnabled(isEnabled: Boolean) = + defaultPrefs.edit { + putBoolean(SETTINGS_ENABLE_RICH_TEXT_FORMATTING_KEY, isEnabled) + } + /** * Tells if a confirmation dialog should be displayed before staring a call. */ diff --git a/vector/src/main/res/drawable/ic_text_formatting.xml b/vector/src/main/res/drawable/ic_text_formatting.xml new file mode 100644 index 0000000000..375c459692 --- /dev/null +++ b/vector/src/main/res/drawable/ic_text_formatting.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_text_formatting_disabled.xml b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml new file mode 100644 index 0000000000..bb34211c7a --- /dev/null +++ b/vector/src/main/res/drawable/ic_text_formatting_disabled.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml new file mode 100644 index 0000000000..7a22ab57f8 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_attachment_type_selector.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index 9f49b8f9d6..c5afe1eb44 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -104,13 +104,26 @@ android:background="@drawable/bg_composer_rich_edit_text_single_line" /> + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml index 7aaa9f6a07..1a3023a805 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml @@ -136,13 +136,29 @@ app:layout_constraintEnd_toEndOf="parent" /> + + + + + + () { fun givenCombinedLoginDisabled() { every { isOnboardingCombinedLoginEnabled() } returns false } + + fun givenLocationSharing(isEnabled: Boolean) { + every { isLocationSharingEnabled() } returns isEnabled + } + + fun givenVoiceBroadcast(isEnabled: Boolean) { + every { isVoiceBroadcastEnabled() } returns isEnabled + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 8b0630c24f..cd4f70bf63 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -40,4 +40,7 @@ class FakeVectorPreferences { fun givenIsClientInfoRecordingEnabled(isEnabled: Boolean) { every { instance.isClientInfoRecordingEnabled() } returns isEnabled } + + fun givenTextFormatting(isEnabled: Boolean) = + every { instance.isTextFormattingEnabled() } returns isEnabled } From 2b7560b1e71a2d12904ae604f123ab932beb7abe Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 27 Oct 2022 14:21:14 +0100 Subject: [PATCH 030/679] Add Labs-Z label for rich text editor and migrate to new label naming --- .github/workflows/triage-labelled.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index cdb354be83..a542dacfb9 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -17,7 +17,8 @@ jobs: contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-Tags') + contains(github.event.issue.labels.*.name, 'A-Tags') || + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: actions/github-script@v5 with: @@ -311,7 +312,7 @@ jobs: name: Add labelled issues to PS features team 3 runs-on: ubuntu-latest if: > - contains(github.event.issue.labels.*.name, 'A-Composer-WYSIWYG') + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: octokit/graphql-action@v2.x id: add_to_project From 15583a14aad1fddcf44076a9e354a05dcf8c685c Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 27 Oct 2022 14:30:36 +0100 Subject: [PATCH 031/679] changelog --- changelog.d/7477.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7477.misc diff --git a/changelog.d/7477.misc b/changelog.d/7477.misc new file mode 100644 index 0000000000..2ea83ce81d --- /dev/null +++ b/changelog.d/7477.misc @@ -0,0 +1 @@ +Add Z-Labs label for rich text editor and migrate to new label naming. \ No newline at end of file From 40ea00f865905f087fcd1ef7716baa827feec2c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Oct 2022 15:54:24 +0200 Subject: [PATCH 032/679] Empty commit to trigger CI From 174ba4f4cc68890e333af86eead16e46989123b3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 26 Oct 2022 16:03:06 +0200 Subject: [PATCH 033/679] VoiceBroadcastPlayer - Create player interface and move implementation to dedicated class --- .../java/im/vector/app/core/di/VoiceModule.kt | 31 +++++--- .../features/home/HomeActivityViewModel.kt | 2 +- .../factory/VoiceBroadcastItemFactory.kt | 4 +- .../item/AbsMessageVoiceBroadcastItem.kt | 4 +- .../MessageVoiceBroadcastListeningItem.kt | 2 +- .../MessageVoiceBroadcastRecordingItem.kt | 2 +- .../voicebroadcast/VoiceBroadcastHelper.kt | 9 ++- .../listening/VoiceBroadcastPlayer.kt | 75 +++++++++++++++++++ .../VoiceBroadcastPlayerImpl.kt} | 62 +++++++-------- .../{ => recording}/VoiceBroadcastRecorder.kt | 2 +- .../VoiceBroadcastRecorderQ.kt | 2 +- .../usecase/PauseVoiceBroadcastUseCase.kt | 4 +- .../usecase/ResumeVoiceBroadcastUseCase.kt | 4 +- .../usecase/StartVoiceBroadcastUseCase.kt | 5 +- .../StopOngoingVoiceBroadcastUseCase.kt | 3 +- .../usecase/StopVoiceBroadcastUseCase.kt | 4 +- .../usecase/PauseVoiceBroadcastUseCaseTest.kt | 3 +- .../ResumeVoiceBroadcastUseCaseTest.kt | 3 +- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 3 +- .../usecase/StopVoiceBroadcastUseCaseTest.kt | 3 +- 20 files changed, 156 insertions(+), 71 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt rename vector/src/main/java/im/vector/app/features/voicebroadcast/{VoiceBroadcastPlayer.kt => listening/VoiceBroadcastPlayerImpl.kt} (89%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/VoiceBroadcastRecorder.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/VoiceBroadcastRecorderQ.kt (98%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/PauseVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/ResumeVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/StartVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/StopOngoingVoiceBroadcastUseCase.kt (95%) rename vector/src/main/java/im/vector/app/features/voicebroadcast/{ => recording}/usecase/StopVoiceBroadcastUseCase.kt (95%) diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt index 54d556ea91..30a8565771 100644 --- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt @@ -18,24 +18,33 @@ package im.vector.app.core.di import android.content.Context import android.os.Build +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorderQ +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ import javax.inject.Singleton -@Module @InstallIn(SingletonComponent::class) -object VoiceModule { - @Provides - @Singleton - fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - VoiceBroadcastRecorderQ(context) - } else { - null +@Module +abstract class VoiceModule { + + companion object { + @Provides + @Singleton + fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + VoiceBroadcastRecorderQ(context) + } else { + null + } } } + + @Binds + abstract fun bindVoiceBroadcastPlayer(player: VoiceBroadcastPlayerImpl): VoiceBroadcastPlayer } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index c3abdde022..49f2079625 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -42,7 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences -import im.vector.app.features.voicebroadcast.usecase.StopOngoingVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.StopOngoingVoiceBroadcastUseCase import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 7a7cb73471..56498fa8d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -26,11 +26,11 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_ -import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUserOrDefault diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 45f10b68d0..ba9d582ea4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -25,9 +25,9 @@ import im.vector.app.R import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider -import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.util.MatrixItem abstract class AbsMessageVoiceBroadcastItem : AbsMessageItem() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index a3e7cc55d5..8df7a9d1a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -23,7 +23,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction -import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index e3e86f38e3..17aa1543c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -22,8 +22,8 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 58e7de7f32..dfc8e35422 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -16,10 +16,11 @@ package im.vector.app.features.voicebroadcast -import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase -import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase -import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase -import im.vector.app.features.voicebroadcast.usecase.StopVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt new file mode 100644 index 0000000000..e2870c4011 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.listening + +interface VoiceBroadcastPlayer { + + /** + * The current playing voice broadcast identifier, if any. + */ + val currentVoiceBroadcastId: String? + + /** + * The current playing [State], [State.IDLE] by default. + */ + val playingState: State + + /** + * Start playback of the given voice broadcast. + */ + fun playOrResume(roomId: String, voiceBroadcastId: String) + + /** + * Pause playback of the current voice broadcast, if any. + */ + fun pause() + + /** + * Stop playback of the current voice broadcast, if any, and reset the player state. + */ + fun stop() + + /** + * Add a [Listener] to the given voice broadcast id. + */ + fun addListener(voiceBroadcastId: String, listener: Listener) + + /** + * Remove a [Listener] from the given voice broadcast id. + */ + fun removeListener(voiceBroadcastId: String, listener: Listener) + + /** + * Player states. + */ + enum class State { + PLAYING, + PAUSED, + BUFFERING, + IDLE + } + + /** + * Listener related to [VoiceBroadcastPlayer]. + */ + fun interface Listener { + /** + * Notify about [VoiceBroadcastPlayer.playingState] changes. + */ + fun onStateChanged(state: State) + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt similarity index 89% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 5a04904f69..168b921c2e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast +package im.vector.app.features.voicebroadcast.listening import android.media.AudioAttributes import android.media.MediaPlayer @@ -22,8 +22,14 @@ import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure +import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk +import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId +import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener +import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -43,14 +49,13 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject -import javax.inject.Singleton @Singleton -class VoiceBroadcastPlayer @Inject constructor( +class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, -) { +) : VoiceBroadcastPlayer { private val session get() = sessionHolder.getActiveSession() @@ -75,9 +80,9 @@ class VoiceBroadcastPlayer @Inject constructor( private var currentSequence: Int? = null private var playlist = emptyList() - var currentVoiceBroadcastId: String? = null + override var currentVoiceBroadcastId: String? = null - private var state: State = State.IDLE + override var playingState = State.IDLE @MainThread set(value) { Timber.w("## VoiceBroadcastPlayer state: $field -> $value") @@ -94,22 +99,22 @@ class VoiceBroadcastPlayer @Inject constructor( */ private val listeners: MutableMap> = mutableMapOf() - fun playOrResume(roomId: String, eventId: String) { - val hasChanged = currentVoiceBroadcastId != eventId + override fun playOrResume(roomId: String, voiceBroadcastId: String) { + val hasChanged = currentVoiceBroadcastId != voiceBroadcastId when { - hasChanged -> startPlayback(roomId, eventId) - state == State.PAUSED -> resumePlayback() + hasChanged -> startPlayback(roomId, voiceBroadcastId) + playingState == State.PAUSED -> resumePlayback() else -> Unit } } - fun pause() { + override fun pause() { currentMediaPlayer?.pause() currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) } - state = State.PAUSED + playingState = State.PAUSED } - fun stop() { + override fun stop() { // Stop playback currentMediaPlayer?.stop() currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } @@ -131,7 +136,7 @@ class VoiceBroadcastPlayer @Inject constructor( timelineListener = null // Update state - state = State.IDLE + playingState = State.IDLE // Clear playlist playlist = emptyList() @@ -143,29 +148,29 @@ class VoiceBroadcastPlayer @Inject constructor( /** * Add a [Listener] to the given voice broadcast id. */ - fun addListener(voiceBroadcastId: String, listener: Listener) { + override fun addListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.add(listener) ?: run { listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } - if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(state) else listener.onStateChanged(State.IDLE) + if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE) } /** * Remove a [Listener] from the given voice broadcast id. */ - fun removeListener(voiceBroadcastId: String, listener: Listener) { + override fun removeListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.remove(listener) } private fun startPlayback(roomId: String, eventId: String) { val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") // Stop listening previous voice broadcast if any - if (state != State.IDLE) stop() + if (playingState != State.IDLE) stop() currentRoomId = roomId currentVoiceBroadcastId = eventId - state = State.BUFFERING + playingState = State.BUFFERING val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState if (voiceBroadcastState == VoiceBroadcastState.STOPPED) { @@ -187,7 +192,7 @@ class VoiceBroadcastPlayer @Inject constructor( currentMediaPlayer?.start() currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } currentSequence = sequence - withContext(Dispatchers.Main) { state = State.PLAYING } + withContext(Dispatchers.Main) { playingState = State.PLAYING } nextMediaPlayer = prepareNextMediaPlayer() } catch (failure: Throwable) { Timber.e(failure, "Unable to start playback") @@ -219,7 +224,7 @@ class VoiceBroadcastPlayer @Inject constructor( private fun resumePlayback() { currentMediaPlayer?.start() currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - state = State.PLAYING + playingState = State.PLAYING } private fun updatePlaylist(playlist: List) { @@ -285,7 +290,7 @@ class VoiceBroadcastPlayer @Inject constructor( if (newChunks.isEmpty()) return updatePlaylist(playlist + newChunks) - when (state) { + when (playingState) { State.PLAYING -> { if (nextMediaPlayer == null) { coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } @@ -330,7 +335,7 @@ class VoiceBroadcastPlayer @Inject constructor( // We'll not receive new chunks anymore so we can stop the live listening stop() } else { - state = State.BUFFERING + playingState = State.BUFFERING } } @@ -339,15 +344,4 @@ class VoiceBroadcastPlayer @Inject constructor( return true } } - - enum class State { - PLAYING, - PAUSED, - BUFFERING, - IDLE - } - - fun interface Listener { - fun onStateChanged(state: State) - } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt index 8b69051823..8bc33ed769 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast +package im.vector.app.features.voicebroadcast.recording import androidx.annotation.IntRange import im.vector.app.features.voice.VoiceRecorder diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt similarity index 98% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index 5285dc5e3b..519f1f24aa 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast +package im.vector.app.features.voicebroadcast.recording import android.content.Context import android.media.MediaRecorder diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt index 1430dd8c86..58e1f26f44 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toContent diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt index 2f03d4194c..524b64e095 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toContent diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 2b7ca7b9f1..a1a519a656 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -14,17 +14,18 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import android.content.Context import androidx.core.content.FileProvider import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt index ab4d16ab60..791409b869 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -14,11 +14,12 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.Membership diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt index bc6a3e7be6..da13100609 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package im.vector.app.features.voicebroadcast.usecase +package im.vector.app.features.voicebroadcast.recording.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toContent diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt index 5c42b26c54..a1ec91aab8 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt @@ -17,9 +17,10 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt index a1bc3a04ec..8b66d45dd4 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt @@ -17,9 +17,10 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 217a395076..59929ef0d7 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -17,10 +17,11 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt index ee6b141bd9..4b15f50be9 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt @@ -17,9 +17,10 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder +import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession From 3fcac097d38a27f12eab252ab0469e793e34ac40 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 27 Oct 2022 16:26:13 +0200 Subject: [PATCH 034/679] VoiceBroadcastPlayer - Fetch playlist in dedicated use case and improve player --- .../listening/VoiceBroadcastPlayerImpl.kt | 130 ++++++------------ .../GetLiveVoiceBroadcastChunksUseCase.kt | 130 ++++++++++++++++++ 2 files changed, 174 insertions(+), 86 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 168b921c2e..9afe428e59 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -23,53 +23,42 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk -import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId -import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State +import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent -import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject +import javax.inject.Singleton @Singleton class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, + private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { + private val session get() = sessionHolder.getActiveSession() private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private var voiceBroadcastStateJob: Job? = null - private var currentTimeline: Timeline? = null - set(value) { - field?.removeAllListeners() - field?.dispose() - field = value - } private val mediaPlayerListener = MediaPlayerListener() - private var timelineListener: TimelineListener? = null private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null @@ -79,7 +68,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private var currentSequence: Int? = null + private var fetchPlaylistJob: Job? = null private var playlist = emptyList() + private var isLive: Boolean = false + override var currentVoiceBroadcastId: String? = null override var playingState = State.IDLE @@ -118,6 +110,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Stop playback currentMediaPlayer?.stop() currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } + isLive = false // Release current player release(currentMediaPlayer) @@ -131,9 +124,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( voiceBroadcastStateJob?.cancel() voiceBroadcastStateJob = null - // In case of live broadcast, stop observing new chunks - currentTimeline = null - timelineListener = null + // Do not fetch the playlist anymore + fetchPlaylistJob?.cancel() + fetchPlaylistJob = null // Update state playingState = State.IDLE @@ -141,13 +134,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Clear playlist playlist = emptyList() currentSequence = null + currentRoomId = null currentVoiceBroadcastId = null } - /** - * Add a [Listener] to the given voice broadcast id. - */ override fun addListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.add(listener) ?: run { listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } @@ -155,15 +146,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE) } - /** - * Remove a [Listener] from the given voice broadcast id. - */ override fun removeListener(voiceBroadcastId: String, listener: Listener) { listeners[voiceBroadcastId]?.remove(listener) } private fun startPlayback(roomId: String, eventId: String) { - val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") // Stop listening previous voice broadcast if any if (playingState != State.IDLE) stop() @@ -173,16 +160,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.BUFFERING val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState - if (voiceBroadcastState == VoiceBroadcastState.STOPPED) { - // Get static playlist - updatePlaylist(getExistingChunks(room, eventId)) - startPlayback(false) - } else { - playLiveVoiceBroadcast(room, eventId) - } + isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED + observeIncomingEvents(roomId, eventId) } - private fun startPlayback(isLive: Boolean) { + private fun startPlayback() { val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } val sequence = event.getVoiceBroadcastChunk()?.sequence @@ -201,24 +183,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun playLiveVoiceBroadcast(room: Room, eventId: String) { - room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() ?: error("Cannot retrieve voice broadcast $eventId") - updatePlaylist(getExistingChunks(room, eventId)) - startPlayback(true) - observeIncomingEvents(room, eventId) - } - - private fun getExistingChunks(room: Room, eventId: String): List { - return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId) - .mapNotNull { it.root.asMessageAudioEvent() } - .filter { it.isVoiceBroadcast() } - } - - private fun observeIncomingEvents(room: Room, eventId: String) { - currentTimeline = room.timelineService().createTimeline(null, TimelineSettings(5)).also { timeline -> - timelineListener = TimelineListener(eventId).also { timeline.addListener(it) } - timeline.start() - } + private fun observeIncomingEvents(roomId: String, voiceBroadcastId: String) { + fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId) + .onEach(this::updatePlaylist) + .launchIn(coroutineScope) } private fun resumePlayback() { @@ -229,11 +197,32 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun updatePlaylist(playlist: List) { this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } + onPlaylistUpdated() + } + + private fun onPlaylistUpdated() { + when (playingState) { + State.PLAYING -> { + if (nextMediaPlayer == null) { + coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + } + } + State.PAUSED -> { + if (nextMediaPlayer == null) { + coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + } + } + State.BUFFERING -> { + val newMediaContent = getNextAudioContent() + if (newMediaContent != null) startPlayback() + } + State.IDLE -> startPlayback() + } } private fun getNextAudioContent(): MessageAudioContent? { val nextSequence = currentSequence?.plus(1) - ?: timelineListener?.let { playlist.lastOrNull()?.sequence } + ?: playlist.lastOrNull()?.sequence ?: 1 return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content } @@ -279,37 +268,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private inner class TimelineListener(private val voiceBroadcastId: String) : Timeline.Listener { - override fun onTimelineUpdated(snapshot: List) { - val currentSequences = playlist.map { it.sequence } - val newChunks = snapshot - .mapNotNull { timelineEvent -> - timelineEvent.root.asMessageAudioEvent() - ?.takeIf { it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.sequence !in currentSequences } - } - if (newChunks.isEmpty()) return - updatePlaylist(playlist + newChunks) - - when (playingState) { - State.PLAYING -> { - if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } - } - } - State.PAUSED -> { - if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } - } - } - State.BUFFERING -> { - val newMediaContent = getNextAudioContent() - if (newMediaContent != null) startPlayback(true) - } - State.IDLE -> startPlayback(true) - } - } - } - private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { @@ -329,7 +287,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val roomId = currentRoomId ?: return val voiceBroadcastId = currentVoiceBroadcastId ?: return val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return - val isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED + isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) { // We'll not receive new chunks anymore so we can stop the live listening diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt new file mode 100644 index 0000000000..8fbd32767d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.listening.usecase + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId +import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.sequence +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.runningReduce +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent +import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import javax.inject.Inject + +/** + * Get a [Flow] of [MessageAudioEvent]s related to the given voice broadcast. + */ +class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, +) { + + fun execute(roomId: String, voiceBroadcastId: String): Flow> { + val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow() + val room = session.roomService().getRoom(roomId) ?: return emptyFlow() + val timeline = room.timelineService().createTimeline(null, TimelineSettings(5)) + + // Get initial chunks + val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId) + .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } + + val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId) + val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState + + return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) { + // Just send the existing chunks if voice broadcast is stopped + flowOf(existingChunks) + } else { + // Observe new timeline events if voice broadcast is ongoing + callbackFlow { + // Init with existing chunks + send(existingChunks) + + // Observe new timeline events + val listener = object : Timeline.Listener { + private var lastEventId: String? = null + private var lastSequence: Int? = null + + override fun onTimelineUpdated(snapshot: List) { + val newEvents = lastEventId?.let { eventId -> snapshot.subList(0, snapshot.indexOfFirst { it.eventId == eventId }) } ?: snapshot + + // Detect a potential stopped voice broadcast state event + val stopEvent = newEvents.findStopEvent() + if (stopEvent != null) { + lastSequence = stopEvent.content?.lastChunkSequence + } + + val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId) + + // Notify about new chunks + if (newChunks.isNotEmpty()) { + trySend(newChunks) + } + + // Automatically stop observing the timeline if the last chunk has been received + if (lastSequence != null && newChunks.any { it.sequence == lastSequence }) { + timeline.removeListener(this) + timeline.dispose() + } + + lastEventId = snapshot.firstOrNull()?.eventId + } + } + + timeline.addListener(listener) + timeline.start() + awaitClose { + timeline.removeListener(listener) + timeline.dispose() + } + } + .runningReduce { accumulator: List, value: List -> accumulator.plus(value) } + } + } + + /** + * Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state. + */ + private fun List.findStopEvent(): VoiceBroadcastEvent? = + this.mapNotNull { it.root.asVoiceBroadcastEvent() } + .find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } + + /** + * Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast. + */ + private fun List.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List = + this.mapNotNull { timelineEvent -> + timelineEvent.root.asMessageAudioEvent() + ?.takeIf { + it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && + it.root.senderId == senderId + } + } +} From 14f1925cd357206adc841ea60c01c0ea617b957d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Oct 2022 23:17:24 +0000 Subject: [PATCH 035/679] Bump sonarqube-gradle-plugin from 3.4.0.2513 to 3.5.0.2730 Bumps sonarqube-gradle-plugin from 3.4.0.2513 to 3.5.0.2730. --- updated-dependencies: - dependency-name: org.sonarsource.scanner.gradle:sonarqube-gradle-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6ccb83e703..fb34ff63a0 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ buildscript { classpath libs.gradle.hiltPlugin classpath 'com.google.firebase:firebase-appdistribution-gradle:3.0.3' classpath 'com.google.gms:google-services:4.3.14' - classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513' + classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.2.3" classpath 'org.owasp:dependency-check-gradle:7.3.0' From 62c574b96634709a456615ef71ac143186c545e7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:29:48 +0200 Subject: [PATCH 036/679] Add changelog --- changelog.d/7478.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7478.wip diff --git a/changelog.d/7478.wip b/changelog.d/7478.wip new file mode 100644 index 0000000000..2e6602b16d --- /dev/null +++ b/changelog.d/7478.wip @@ -0,0 +1 @@ +[Voice Broadcast] Improve playlist fetching and player codebase From 838e11c167a5bdf9d4b36ef1221f0518029a94b8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:43:05 +0200 Subject: [PATCH 037/679] rename observeIncomingEvents method and reorder some methods --- .../listening/VoiceBroadcastPlayerImpl.kt | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 9afe428e59..3999a0e0af 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -161,40 +161,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED - observeIncomingEvents(roomId, eventId) + fetchPlaylistAndStartPlayback(roomId, eventId) } - private fun startPlayback() { - val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() - val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = event.getVoiceBroadcastChunk()?.sequence - coroutineScope.launch { - try { - currentMediaPlayer = prepareMediaPlayer(content) - currentMediaPlayer?.start() - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - currentSequence = sequence - withContext(Dispatchers.Main) { playingState = State.PLAYING } - nextMediaPlayer = prepareNextMediaPlayer() - } catch (failure: Throwable) { - Timber.e(failure, "Unable to start playback") - throw VoiceFailure.UnableToPlay(failure) - } - } - } - - private fun observeIncomingEvents(roomId: String, voiceBroadcastId: String) { + private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) { fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId) .onEach(this::updatePlaylist) .launchIn(coroutineScope) } - private fun resumePlayback() { - currentMediaPlayer?.start() - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - playingState = State.PLAYING - } - private fun updatePlaylist(playlist: List) { this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } onPlaylistUpdated() @@ -220,6 +195,31 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } + private fun startPlayback() { + val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() + val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } + val sequence = event.getVoiceBroadcastChunk()?.sequence + coroutineScope.launch { + try { + currentMediaPlayer = prepareMediaPlayer(content) + currentMediaPlayer?.start() + currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } + currentSequence = sequence + withContext(Dispatchers.Main) { playingState = State.PLAYING } + nextMediaPlayer = prepareNextMediaPlayer() + } catch (failure: Throwable) { + Timber.e(failure, "Unable to start playback") + throw VoiceFailure.UnableToPlay(failure) + } + } + } + + private fun resumePlayback() { + currentMediaPlayer?.start() + currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } + playingState = State.PLAYING + } + private fun getNextAudioContent(): MessageAudioContent? { val nextSequence = currentSequence?.plus(1) ?: playlist.lastOrNull()?.sequence From 362696cfc88da4a67d4a527fccc9917d6508f124 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:27:05 +0200 Subject: [PATCH 038/679] VoiceBroadcast - Show error dialog if user is not able to record a voice broadcast --- .../src/main/res/values/strings.xml | 4 +++ .../vector/app/core/error/ErrorFormatter.kt | 11 +++++++ .../home/room/detail/TimelineFragment.kt | 7 +++- .../home/room/detail/TimelineViewModel.kt | 7 +++- .../voicebroadcast/VoiceBroadcastFailure.kt | 25 +++++++++++++++ .../usecase/StartVoiceBroadcastUseCase.kt | 32 ++++++++++++++++--- 6 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 9edd7d836a..b5abefec94 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3085,6 +3085,10 @@ Play or resume voice broadcast Pause voice broadcast Buffering + Can’t start a new voice broadcast + You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. + Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. + You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index a09f852958..380c80775b 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -21,6 +21,8 @@ import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.voice.VoiceFailure +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixIdFailure @@ -135,6 +137,7 @@ class DefaultErrorFormatter @Inject constructor( is MatrixIdFailure.InvalidMatrixId -> stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id) is VoiceFailure -> voiceMessageError(throwable) + is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable) is ActivityNotFoundException -> stringProvider.getString(R.string.error_no_external_application_found) else -> throwable.localizedMessage @@ -149,6 +152,14 @@ class DefaultErrorFormatter @Inject constructor( } } + private fun voiceBroadcastMessageError(throwable: VoiceBroadcastFailure): String { + return when (throwable) { + RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message) + RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message) + RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message) + } + } + private fun limitExceededError(error: MatrixError): String { val delay = error.retryAfterMillis diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 4f51922a62..b259d51947 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -33,6 +33,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.activity.addCallback +import androidx.annotation.StringRes import androidx.appcompat.view.menu.MenuBuilder import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat @@ -1320,8 +1321,12 @@ class TimelineFragment : } private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) { + @StringRes val titleResId = when(result.action) { + RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title + else -> R.string.dialog_title_error + } MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.dialog_title_error) + .setTitle(titleResId) .setMessage(errorFormatter.toHumanReadable(result.throwable)) .setPositiveButton(R.string.ok, null) .show() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 82ad96d645..ac117558be 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -604,7 +604,12 @@ class TimelineViewModel @AssistedInject constructor( if (room == null) return viewModelScope.launch { when (action) { - RoomDetailAction.VoiceBroadcastAction.Recording.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId) + RoomDetailAction.VoiceBroadcastAction.Recording.Start -> { + voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold( + { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) }, + { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) }, + ) + } RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt new file mode 100644 index 0000000000..76b50c78ab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast + +sealed class VoiceBroadcastFailure : Throwable() { + sealed class RecordingError : VoiceBroadcastFailure() { + object NoPermission : RecordingError() + object BlockedBySomeoneElse : RecordingError() + object UserAlreadyBroadcasting : RecordingError() + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index a1a519a656..f6870f859f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -24,15 +24,22 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.getStateEvent +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import timber.log.Timber import java.io.File import javax.inject.Inject @@ -50,12 +57,27 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") - val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId) + val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.let { PowerLevelsHelper(it) } - if (onGoingVoiceBroadcastEvents.isEmpty()) { - startVoiceBroadcast(room) - } else { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents") + when { + powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission") + throw VoiceBroadcastFailure.RecordingError.NoPermission + } + voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") + throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting + } + getOngoingVoiceBroadcastsUseCase.execute(roomId).isNotEmpty() -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") + throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse + } + else -> { + startVoiceBroadcast(room) + } } } From b510919d59d92466f29f68d7c6cfd79c0b6e363b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 10:53:35 +0200 Subject: [PATCH 039/679] Add changelog --- changelog.d/7485.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7485.wip diff --git a/changelog.d/7485.wip b/changelog.d/7485.wip new file mode 100644 index 0000000000..30cab45d9c --- /dev/null +++ b/changelog.d/7485.wip @@ -0,0 +1 @@ +[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast From d1f5fa5b59a439742ac8ccd3f228513ede310736 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 23:06:32 +0000 Subject: [PATCH 040/679] Bump flipper from 0.171.1 to 0.173.0 Bumps `flipper` from 0.171.1 to 0.173.0. Updates `flipper` from 0.171.1 to 0.173.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.171.1...v0.173.0) Updates `flipper-network-plugin` from 0.171.1 to 0.173.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.171.1...v0.173.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 33a2096a43..1bc9be92a4 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.171.1" +def flipper = "0.173.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" From 3a430efb0289f7382b009473d06acc5d02cbc913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Aguirrezabalaga?= Date: Sun, 30 Oct 2022 11:32:52 +0100 Subject: [PATCH 041/679] Add setting to allow disabling direct share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direct share continues to be enabled by default. As requested in #2725 Signed-off-by: Joaquín Aguirrezabalaga --- changelog.d/2725.feature | 1 + library/ui-strings/src/main/res/values/strings.xml | 2 ++ .../im/vector/app/features/home/ShortcutCreator.kt | 12 ++++++++---- .../app/features/settings/VectorPreferences.kt | 5 +++++ .../src/main/res/xml/vector_settings_preferences.xml | 6 ++++++ 5 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2725.feature diff --git a/changelog.d/2725.feature b/changelog.d/2725.feature new file mode 100644 index 0000000000..eb3fcaed57 --- /dev/null +++ b/changelog.d/2725.feature @@ -0,0 +1 @@ +Add setting to allow disabling direct share diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 897c2853d8..18d80f7aa4 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -1032,6 +1032,8 @@ Use /confetti command or send a message containing ❄️ or 🎉 Autoplay animated images Play animated images in the timeline as soon as they are visible + Enable direct share + Show recent chats in the system share menu Show join and leave events Invites, removes, and bans are unaffected. Show account events diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt index e0565debf2..a0bcea217f 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutCreator.kt @@ -29,6 +29,7 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.core.resources.BuildMeta import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.MainActivity +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -55,6 +56,7 @@ class ShortcutCreator @Inject constructor( dimensionConverter.dpToPx(72) } } + @Inject lateinit var vectorPreferences: VectorPreferences fun canCreateShortcut(): Boolean { return ShortcutManagerCompat.isRequestPinShortcutSupported(context) @@ -73,10 +75,12 @@ class ShortcutCreator @Inject constructor( } catch (failure: Throwable) { null } - val categories = if (Build.VERSION.SDK_INT >= 25) { - setOf(directShareCategory, ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION) - } else { - setOf(directShareCategory) + val categories = mutableSetOf() + if (vectorPreferences.directShareEnabled()) { + categories.add(directShareCategory) + } + if (Build.VERSION.SDK_INT >= 25) { + categories.add(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION) } return ShortcutInfoCompat.Builder(context, roomSummary.roomId) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 2dc8b12160..8cfc6a5baa 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -123,6 +123,7 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_LABS_ENABLE_LATEX_MATHS = "SETTINGS_LABS_ENABLE_LATEX_MATHS" const val SETTINGS_PRESENCE_USER_ALWAYS_APPEARS_OFFLINE = "SETTINGS_PRESENCE_USER_ALWAYS_APPEARS_OFFLINE" const val SETTINGS_AUTOPLAY_ANIMATED_IMAGES = "SETTINGS_AUTOPLAY_ANIMATED_IMAGES" + private const val SETTINGS_ENABLE_DIRECT_SHARE = "SETTINGS_ENABLE_DIRECT_SHARE" // Room directory private const val SETTINGS_ROOM_DIRECTORY_SHOW_ALL_PUBLIC_ROOMS = "SETTINGS_ROOM_DIRECTORY_SHOW_ALL_PUBLIC_ROOMS" @@ -1004,6 +1005,10 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_ENABLE_CHAT_EFFECTS, true) } + fun directShareEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_ENABLE_DIRECT_SHARE, true) + } + /** * Return true if Pin code is disabled, or if user set the settings to see full notification content. */ diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 172fa5606c..52afe16bbb 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -147,6 +147,12 @@ android:title="@string/settings_vibrate_on_mention" app:isPreferenceVisible="@bool/false_not_implemented" /> + + Date: Mon, 31 Oct 2022 10:58:09 +0100 Subject: [PATCH 042/679] Fix lint issues --- .../im/vector/app/features/home/room/detail/TimelineFragment.kt | 2 +- .../recording/usecase/StartVoiceBroadcastUseCase.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index b259d51947..120e5e22cb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1321,7 +1321,7 @@ class TimelineFragment : } private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) { - @StringRes val titleResId = when(result.action) { + @StringRes val titleResId = when (result.action) { RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title else -> R.string.dialog_title_error } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index f6870f859f..8a335eccac 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -21,10 +21,10 @@ import androidx.core.content.FileProvider import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType From d7791402b79d63c63566564b95af94e404c0bdc8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 15:18:24 +0100 Subject: [PATCH 043/679] Fix unit tests --- .../usecase/StartVoiceBroadcastUseCase.kt | 57 ++++++++++++------- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 29 +++++++--- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 8a335eccac..85f72c09da 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -28,6 +28,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import org.jetbrains.annotations.VisibleForTesting import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType @@ -57,28 +58,8 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") - val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - ?.let { PowerLevelsHelper(it) } - - when { - powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true -> { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission") - throw VoiceBroadcastFailure.RecordingError.NoPermission - } - voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") - throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting - } - getOngoingVoiceBroadcastsUseCase.execute(roomId).isNotEmpty() -> { - Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") - throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse - } - else -> { - startVoiceBroadcast(room) - } - } + assertCanStartVoiceBroadcast(room) + startVoiceBroadcast(room) } private suspend fun startVoiceBroadcast(room: Room) { @@ -124,4 +105,36 @@ class StartVoiceBroadcastUseCase @Inject constructor( ) ) } + + private fun assertCanStartVoiceBroadcast(room: Room) { + assertHasEnoughPowerLevels(room) + assertNoOngoingVoiceBroadcast(room) + } + + @VisibleForTesting + fun assertHasEnoughPowerLevels(room: Room) { + val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.let { PowerLevelsHelper(it) } + + if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission") + throw VoiceBroadcastFailure.RecordingError.NoPermission + } + } + + @VisibleForTesting + fun assertNoOngoingVoiceBroadcast(room: Room) { + when { + voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") + throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting + } + getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> { + Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting") + throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse + } + } + } } diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 59929ef0d7..ef78f1c80d 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -26,15 +26,17 @@ import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeSession -import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.slot +import io.mockk.spyk import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeNull +import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -51,14 +53,23 @@ class StartVoiceBroadcastUseCaseTest { private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) private val fakeGetOngoingVoiceBroadcastsUseCase = mockk() - private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( - session = fakeSession, - voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, - context = FakeContext().instance, - buildMeta = mockk(), - getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, + private val startVoiceBroadcastUseCase = spyk( + StartVoiceBroadcastUseCase( + session = fakeSession, + voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, + context = FakeContext().instance, + buildMeta = mockk(), + getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, + ) ) + @Before + fun setup() { + every { fakeRoom.roomId } returns A_ROOM_ID + justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) } + every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle + } + @Test fun `given a room id with potential several existing voice broadcast states when calling execute then the voice broadcast is started or not`() = runTest { val cases = VoiceBroadcastState.values() @@ -83,7 +94,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List) { // Given - clearAllMocks() + setup() givenVoiceBroadcasts(voiceBroadcasts) val voiceBroadcastInfoContentInterceptor = slot() coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID } @@ -106,7 +117,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List) { // Given - clearAllMocks() + setup() givenVoiceBroadcasts(voiceBroadcasts) // When From 7ba1052bcf6311547ac0ca9ecc1b09720bd0c9b9 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 31 Oct 2022 16:43:01 +0100 Subject: [PATCH 044/679] Fix rich text editor EditText not resizing properly in full screen (#7491) * Fix rich text editor full screen mode * Add changelog * Address review comments. --- changelog.d/7491.bugfix | 1 + .../detail/composer/RichTextComposerLayout.kt | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 changelog.d/7491.bugfix diff --git a/changelog.d/7491.bugfix b/changelog.d/7491.bugfix new file mode 100644 index 0000000000..1a87bd03bd --- /dev/null +++ b/changelog.d/7491.bugfix @@ -0,0 +1 @@ +Fix rich text editor textfield not growing to fill parent on full screen. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 2c09f351bb..2d2a4a8cd2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -54,8 +54,9 @@ class RichTextComposerLayout @JvmOverloads constructor( private var currentConstraintSetId: Int = -1 private val animationDuration = 100L + private val maxEditTextLinesWhenCollapsed = 12 - private var isFullScreen = false + private val isFullScreen: Boolean get() = currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_fullscreen var isTextFormattingEnabled = true set(value) { @@ -104,10 +105,10 @@ class RichTextComposerLayout @JvmOverloads constructor( collapse(false) views.richTextComposerEditText.addTextChangedListener( - TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() }) ) views.plainTextComposerEditText.addTextChangedListener( - TextChangeListener({ callback?.onTextChanged(it) }, ::updateTextFieldBorder) + TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() }) ) views.composerRelatedMessageCloseButton.setOnClickListener { @@ -196,8 +197,9 @@ class RichTextComposerLayout @JvmOverloads constructor( button.isSelected = menuState.reversedActions.contains(action) } - private fun updateTextFieldBorder(isExpanded: Boolean) { - val borderResource = if (isExpanded) { + private fun updateTextFieldBorder() { + val isExpanded = editText.editableText.lines().count() > 1 + val borderResource = if (isExpanded || isFullScreen) { R.drawable.bg_composer_rich_edit_text_expanded } else { R.drawable.bg_composer_rich_edit_text_single_line @@ -240,8 +242,21 @@ class RichTextComposerLayout @JvmOverloads constructor( it.applyTo(this) } - updateTextFieldBorder(newValue) + updateTextFieldBorder() updateEditTextVisibility() + + updateEditTextFullScreenState(views.richTextComposerEditText, newValue) + updateEditTextFullScreenState(views.plainTextComposerEditText, newValue) + } + + private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) { + if (isFullScreen) { + editText.maxLines = Int.MAX_VALUE + // This is a workaround to fix incorrect scroll position when maximised + post { editText.requestLayout() } + } else { + editText.maxLines = maxEditTextLinesWhenCollapsed + } } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { From d4234ae3bd6b63c0d126b940fdbefd7acb06a4b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:08:36 +0000 Subject: [PATCH 045/679] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b6333c5940..a44872e0ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build docs run: ./gradlew dokkaHtml From d2012ae0222bb21f8692aa0e9a5c75dafe8d913d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:10:15 +0000 Subject: [PATCH 046/679] Bump lazythreetenbp from 0.11.0 to 0.12.0 Bumps [lazythreetenbp](https://github.com/gabrielittner/lazythreetenbp) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/gabrielittner/lazythreetenbp/releases) - [Changelog](https://github.com/gabrielittner/lazythreetenbp/blob/main/CHANGELOG.md) - [Commits](https://github.com/gabrielittner/lazythreetenbp/compare/0.11.0...0.12.0) --- updated-dependencies: - dependency-name: com.gabrielittner.threetenbp:lazythreetenbp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 0b884f4d99..90992205e3 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -132,7 +132,7 @@ dependencies { implementation libs.androidx.biometric api "org.threeten:threetenbp:1.4.0:no-tzdb" - api "com.gabrielittner.threetenbp:lazythreetenbp:0.11.0" + api "com.gabrielittner.threetenbp:lazythreetenbp:0.12.0" implementation libs.squareup.moshi kapt libs.squareup.moshiKotlin From 411c8c90961d83a7e5bea10e5c055b6245ee7d92 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 1 Nov 2022 11:21:27 +0100 Subject: [PATCH 047/679] Fix duplicated pills when pills contain other spans Fixes following issues: - Duplicated pills if the mention contains an image: https://github.com/SchildiChat/SchildiChat-android/issues/148 - Duplicated pills if these contain underscores: https://github.com/SchildiChat/SchildiChat-android/issues/156 --- .../vector/app/features/html/PillsPostProcessor.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt index 85cfb76ff7..f6e10a6df9 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt @@ -83,6 +83,20 @@ class PillsPostProcessor @AssistedInject constructor( val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach val startSpan = renderedText.getSpanStart(linkSpan) val endSpan = renderedText.getSpanEnd(linkSpan) + // GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span, + // such as images or italic text. + // Accordingly, it's better to remove all spans that are contained in this span before rendering. + renderedText.getSpans(startSpan, endSpan, Any::class.java).forEach remove@{ + if (it !is LinkSpan) { + // Make sure to only remove spans that are contained in this link, and not are bigger than this link, e.g. like reply-blocks + val start = renderedText.getSpanStart(it) + if (start < startSpan) return@remove + val end = renderedText.getSpanEnd(it) + if (end > endSpan) return@remove + + renderedText.removeSpan(it) + } + } addPillSpan(renderedText, pillSpan, startSpan, endSpan) } } From 99d510773269be972b9b8926b9584816d07a552b Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Tue, 1 Nov 2022 15:49:46 +0100 Subject: [PATCH 048/679] Changelog 7501 --- changelog.d/7501.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7501.bugfix diff --git a/changelog.d/7501.bugfix b/changelog.d/7501.bugfix new file mode 100644 index 0000000000..b86258d427 --- /dev/null +++ b/changelog.d/7501.bugfix @@ -0,0 +1 @@ +Fix duplicated mention pills in some cases From 20abef26b0a6eda4ef7d600072a64f4587cffba0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 18:19:21 +0100 Subject: [PATCH 049/679] Filter duplicated events in live voice broadcasts --- .../listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 8fbd32767d..4f9f2de673 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningReduce import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent @@ -106,6 +107,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( } } .runningReduce { accumulator: List, value: List -> accumulator.plus(value) } + .map { events -> events.distinctBy { it.sequence } } } } From 68062911a98da9e23c1f7c0b47b6a166973eee8f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 1 Nov 2022 18:17:23 +0100 Subject: [PATCH 050/679] Changelog --- changelog.d/7502.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7502.bugfix diff --git a/changelog.d/7502.bugfix b/changelog.d/7502.bugfix new file mode 100644 index 0000000000..8785310498 --- /dev/null +++ b/changelog.d/7502.bugfix @@ -0,0 +1 @@ +Voice Broadcast - Fix duplicated voice messages in the internal playlist From 38fe5569789931b72ca6ecce6571a8317df1caff Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:30:44 +0200 Subject: [PATCH 051/679] Adding changelog entry --- changelog.d/7457.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7457.bugfix diff --git a/changelog.d/7457.bugfix b/changelog.d/7457.bugfix new file mode 100644 index 0000000000..9dfbc53329 --- /dev/null +++ b/changelog.d/7457.bugfix @@ -0,0 +1 @@ +[Session manager] Hide push notification toggle when there is no server support From 1acb42f61d95b6dbbe2a9392312c40aeaeb6b933 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 11:34:41 +0200 Subject: [PATCH 052/679] Adding use case to check support for new enabled field support --- .../homeserver/HomeServerCapabilities.kt | 5 ++ .../sdk/internal/auth/version/Versions.kt | 11 ++++ .../database/RealmSessionStoreMigration.kt | 4 +- .../mapper/HomeServerCapabilitiesMapper.kt | 3 +- .../database/migration/MigrateSessionTo042.kt | 31 ++++++++++ .../model/HomeServerCapabilitiesEntity.kt | 1 + .../GetHomeServerCapabilitiesTask.kt | 12 +++- ...TogglePushNotificationsViaPusherUseCase.kt | 35 +++++++++++ ...lePushNotificationsViaPusherUseCaseTest.kt | 61 +++++++++++++++++++ .../fixtures/HomeserverCapabilityFixture.kt | 20 +++--- 10 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 773e870ffd..11638837cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -70,6 +70,11 @@ data class HomeServerCapabilities( * True if the home server supports threaded read receipts and unread notifications. */ val canUseThreadReadReceiptsAndNotifications: Boolean = false, + + /** + * True if the home server supports remote toggle of Pusher for a given device. + */ + val canRemotelyTogglePushNotificationsOfDevices: Boolean = false, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 1245d8df4b..bc2d4a5aef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.version import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse /** * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions. @@ -56,6 +57,7 @@ private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" +private const val FEATURE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" /** * Return true if the SDK supports this homeserver version. @@ -142,3 +144,12 @@ private fun Versions.getMaxVersion(): HomeServerVersion { ?.maxOrNull() ?: HomeServerVersion.r0_0_0 } + +/** + * Indicate if the server supports MSC3881: https://github.com/matrix-org/matrix-spec-proposals/pull/3881. + * + * @return true if remote toggle of push notifications is supported + */ +internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean { + return unstableFeatures?.get(FEATURE_PUSH_NOTIFICATIONS_MSC3881).orFalse() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 58c015b13b..30836c027e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -58,6 +58,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -66,7 +67,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 41L, + schemaVersion = 42L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -117,5 +118,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 39) MigrateSessionTo039(realm).perform() if (oldVersion < 40) MigrateSessionTo040(realm).perform() if (oldVersion < 41) MigrateSessionTo041(realm).perform() + if (oldVersion < 42) MigrateSessionTo042(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 3528ca0051..89657ad882 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -45,7 +45,8 @@ internal object HomeServerCapabilitiesMapper { canUseThreading = entity.canUseThreading, canControlLogoutDevices = entity.canControlLogoutDevices, canLoginWithQrCode = entity.canLoginWithQrCode, - canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications + canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, + canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt new file mode 100644 index 0000000000..8826d894c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo042(realm: DynamicRealm) : RealmMigrator(realm, 42) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.CAN_REMOTELY_TOGGLE_PUSH_NOTIFICATIONS_OF_DEVICES, Boolean::class.java) + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 89f1e50b30..2b60f7723c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -33,6 +33,7 @@ internal open class HomeServerCapabilitiesEntity( var canControlLogoutDevices: Boolean = false, var canLoginWithQrCode: Boolean = false, var canUseThreadReadReceiptsAndNotifications: Boolean = false, + var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index a5953d870c..11e86a5c51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin +import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk @@ -141,13 +142,18 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } if (getVersionResult != null) { - homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() - homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices() + homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = + getVersionResult.isLoginAndRegistrationSupportedBySdk() + homeServerCapabilitiesEntity.canControlLogoutDevices = + getVersionResult.doesServerSupportLogoutDevices() homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ getVersionResult.doesServerSupportThreads() homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications = getVersionResult.doesServerSupportThreadUnreadNotifications() - homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin() + homeServerCapabilitiesEntity.canLoginWithQrCode = + getVersionResult.doesServerSupportQrCodeLogin() + homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = + getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt new file mode 100644 index 0000000000..0d5bce663a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(): Boolean { + return activeSessionHolder + .getSafeActiveSession() + ?.homeServerCapabilitiesService() + ?.getHomeServerCapabilities() + ?.canRemotelyTogglePushNotificationsOfDevices + .orFalse() + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt new file mode 100644 index 0000000000..51874be1bc --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fixtures.aHomeServerCapabilities +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) + +class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanTogglePushNotificationsViaPusherUseCase = + CheckIfCanTogglePushNotificationsViaPusherUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given current session when execute then toggle capability is returned`() { + // Given + fakeActiveSessionHolder + .fakeSession + .fakeHomeServerCapabilitiesService + .givenCapabilities(A_HOMESERVER_CAPABILITIES) + + // When + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + + // Then + result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices + } + + @Test + fun `given no current session when execute then false is returned`() { + // Given + fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) + + // When + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + + // Then + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt index a4d9869a89..c9f32c2cf2 100644 --- a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt +++ b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt @@ -27,14 +27,16 @@ fun aHomeServerCapabilities( maxUploadFileSize: Long = 100L, lastVersionIdentityServerSupported: Boolean = false, defaultIdentityServerUrl: String? = null, - roomVersions: RoomVersionCapabilities? = null + roomVersions: RoomVersionCapabilities? = null, + canRemotelyTogglePushNotificationsOfDevices: Boolean = true, ) = HomeServerCapabilities( - canChangePassword, - canChangeDisplayName, - canChangeAvatar, - canChange3pid, - maxUploadFileSize, - lastVersionIdentityServerSupported, - defaultIdentityServerUrl, - roomVersions + canChangePassword = canChangePassword, + canChangeDisplayName = canChangeDisplayName, + canChangeAvatar = canChangeAvatar, + canChange3pid = canChange3pid, + maxUploadFileSize = maxUploadFileSize, + lastVersionIdentityServerSupported = lastVersionIdentityServerSupported, + defaultIdentityServerUrl = defaultIdentityServerUrl, + roomVersions = roomVersions, + canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices, ) From 62912f891cde60bd07fd73735dff126816d85910 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 14:36:06 +0200 Subject: [PATCH 053/679] Introducing a NotificationsStatus to render the push notification toggle in session overview screen --- ...ePushNotificationsViaAccountDataUseCase.kt | 33 +++++++++ .../GetNotificationsStatusUseCase.kt | 59 +++++++++++++++ .../v2/notification/NotificationsStatus.kt | 23 ++++++ .../TogglePushNotificationUseCase.kt | 16 +++-- .../v2/overview/SessionOverviewFragment.kt | 22 +++--- .../v2/overview/SessionOverviewViewModel.kt | 33 +++------ .../v2/overview/SessionOverviewViewState.kt | 3 +- ...hNotificationsViaAccountDataUseCaseTest.kt | 71 +++++++++++++++++++ .../TogglePushNotificationUseCaseTest.kt | 26 ++++++- .../overview/SessionOverviewViewModelTest.kt | 23 +++--- .../fakes/FakeSessionAccountDataService.kt | 4 +- .../FakeTogglePushNotificationUseCase.kt | 2 +- 12 files changed, 264 insertions(+), 51 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt rename vector/src/main/java/im/vector/app/features/settings/devices/v2/{overview => notification}/TogglePushNotificationUseCase.kt (67%) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt rename vector/src/test/java/im/vector/app/features/settings/devices/v2/{overview => notification}/TogglePushNotificationUseCaseTest.kt (67%) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt new file mode 100644 index 0000000000..dbf9adca14 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import javax.inject.Inject + +class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(deviceId: String): Boolean { + return activeSessionHolder + .getSafeActiveSession() + ?.accountDataService() + ?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt new file mode 100644 index 0000000000..1eb612988a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap +import javax.inject.Inject + +class GetNotificationsStatusUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, +) { + + // TODO add unit tests + fun execute(deviceId: String): Flow { + val session = activeSessionHolder.getSafeActiveSession() + return when { + session == null -> emptyFlow() + checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + } + checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { + session.flow() + .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + .unwrap() + .map { it.content.toModel()?.isSilenced?.not() } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + } + else -> flowOf(NotificationsStatus.NOT_SUPPORTED) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt new file mode 100644 index 0000000000..7ff1f04381 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +enum class NotificationsStatus { + ENABLED, + DISABLED, + NOT_SUPPORTED, +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt similarity index 67% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 45c234aaef..be9012e9f1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2.overview +package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -24,17 +24,21 @@ import javax.inject.Inject class TogglePushNotificationUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return - val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } - devicePusher?.let { pusher -> - session.pushersService().togglePusher(pusher, enabled) + + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } + devicePusher?.let { pusher -> + session.pushersService().togglePusher(pusher, enabled) + } } - val accountData = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - if (accountData != null) { + if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) session.accountDataService().updateUserAccountData( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index a1cd7ea586..620372f810 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -43,6 +44,7 @@ import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -177,7 +179,7 @@ class SessionOverviewFragment : updateEntryDetails(state.deviceId) updateSessionInfo(state) updateLoading(state.isLoading) - updatePushNotificationToggle(state.deviceId, state.notificationsEnabled) + updatePushNotificationToggle(state.deviceId, state.notificationsStatus) } private fun updateToolbar(viewState: SessionOverviewViewState) { @@ -218,15 +220,19 @@ class SessionOverviewFragment : } } - private fun updatePushNotificationToggle(deviceId: String, enabled: Boolean) { - views.sessionOverviewPushNotifications.apply { - setOnCheckedChangeListener(null) - setChecked(enabled) - post { - setOnCheckedChangeListener { _, isChecked -> - viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked)) + private fun updatePushNotificationToggle(deviceId: String, notificationsStatus: NotificationsStatus) { + views.sessionOverviewPushNotifications.isGone = notificationsStatus == NotificationsStatus.NOT_SUPPORTED + when (notificationsStatus) { + NotificationsStatus.ENABLED, NotificationsStatus.DISABLED -> { + views.sessionOverviewPushNotifications.setOnCheckedChangeListener(null) + views.sessionOverviewPushNotifications.setChecked(notificationsStatus == NotificationsStatus.ENABLED) + views.sessionOverviewPushNotifications.post { + views.sessionOverviewPushNotifications.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked)) + } } } + else -> Unit } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 21054270f8..1aa5f676cc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -29,6 +29,9 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase @@ -36,21 +39,15 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation @@ -65,6 +62,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase @@ -81,7 +79,7 @@ class SessionOverviewViewModel @AssistedInject constructor( refreshPushers() observeSessionInfo(initialState.deviceId) observeCurrentSessionInfo() - observePushers(initialState.deviceId) + observeNotificationsStatus(initialState.deviceId) } private fun refreshPushers() { @@ -107,20 +105,9 @@ class SessionOverviewViewModel @AssistedInject constructor( } } - private fun observePushers(deviceId: String) { - val session = activeSessionHolder.getSafeActiveSession() ?: return - val pusherFlow = session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - - val accountDataFlow = session.flow() - .liveUserAccountData(TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - .unwrap() - .map { it.content.toModel()?.isSilenced?.not() } - - merge(pusherFlow, accountDataFlow) - .onEach { it?.let { setState { copy(notificationsEnabled = it) } } } + private fun observeNotificationsStatus(deviceId: String) { + getNotificationsStatusUseCase.execute(deviceId) + .onEach { setState { copy(notificationsStatus = it) } } .launchIn(viewModelScope) } @@ -233,7 +220,9 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { togglePushNotificationUseCase.execute(action.deviceId, action.enabled) - setState { copy(notificationsEnabled = action.enabled) } + // TODO should not be needed => test without + val status = if (action.enabled) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED + setState { copy(notificationsStatus = status) } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt index 440805bad6..019dd2d724 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt @@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus data class SessionOverviewViewState( val deviceId: String, val isCurrentSessionTrusted: Boolean = false, val deviceInfo: Async = Uninitialized, val isLoading: Boolean = false, - val notificationsEnabled: Boolean = false, + val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED, ) : MavericksState { constructor(args: SessionOverviewArgs) : this( deviceId = args.deviceId diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..0303444605 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes + +private const val A_DEVICE_ID = "device-id" + +class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase = + CheckIfCanTogglePushNotificationsViaAccountDataUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given current session and an account data for the device id when execute then result is true`() { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = mockk(), + ) + + // When + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + + // Then + result shouldBeEqualTo true + } + + @Test + fun `given current session and NO account data for the device id when execute then result is false`() { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = null, + ) + + // When + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + + // Then + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt similarity index 67% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt index dc64c74836..0a649354f9 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt @@ -14,10 +14,12 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2.overview +package im.vector.app.features.settings.devices.v2.notification import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fixtures.PusherFixture +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -27,10 +29,21 @@ import org.matrix.android.sdk.api.session.events.model.toContent class TogglePushNotificationUseCaseTest { private val activeSessionHolder = FakeActiveSessionHolder() - private val togglePushNotificationUseCase = TogglePushNotificationUseCase(activeSessionHolder.instance) + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = + mockk() + + private val togglePushNotificationUseCase = + TogglePushNotificationUseCase( + activeSessionHolder = activeSessionHolder.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + ) @Test fun `when execute, then toggle enabled for device pushers`() = runTest { + // Given val sessionId = "a_session_id" val pushers = listOf( PusherFixture.aPusher(deviceId = sessionId, enabled = false), @@ -38,14 +51,19 @@ class TogglePushNotificationUseCaseTest { ) activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns false + // When togglePushNotificationUseCase.execute(sessionId, true) + // Then activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true) } @Test fun `when execute, then toggle local notification settings`() = runTest { + // Given val sessionId = "a_session_id" val pushers = listOf( PusherFixture.aPusher(deviceId = sessionId, enabled = false), @@ -56,9 +74,13 @@ class TogglePushNotificationUseCaseTest { UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = true).toContent() ) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns true + // When togglePushNotificationUseCase.execute(sessionId, true) + // Then activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = false).toContent(), diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 544059b77f..c0ba6ce28b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -23,6 +23,8 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase @@ -32,7 +34,6 @@ import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService -import im.vector.app.test.fixtures.PusherFixture.aPusher import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.coEvery @@ -87,6 +88,8 @@ class SessionOverviewViewModelTest { private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk() private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() + private val fakeGetNotificationsStatusUseCase = mockk() + private val notificationsStatus = NotificationsStatus.ENABLED private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), @@ -99,6 +102,7 @@ class SessionOverviewViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, + getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase, ) @Before @@ -108,6 +112,7 @@ class SessionOverviewViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() + every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) } @After @@ -131,7 +136,7 @@ class SessionOverviewViewModelTest { deviceId = A_SESSION_ID_1, deviceInfo = Success(deviceFullInfo), isCurrentSessionTrusted = true, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) val viewModel = createViewModel() @@ -227,7 +232,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) // When @@ -264,7 +269,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) @@ -299,7 +304,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) @@ -466,13 +471,13 @@ class SessionOverviewViewModelTest { @Test fun `when viewModel init, then observe pushers and emit to state`() { - val pushers = listOf(aPusher(deviceId = A_SESSION_ID_1)) - fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) + val notificationStatus = NotificationsStatus.ENABLED + every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationStatus) val viewModel = createViewModel() viewModel.test() - .assertLatestState { state -> state.notificationsEnabled } + .assertLatestState { state -> state.notificationsStatus == notificationStatus } .finish() } @@ -483,6 +488,6 @@ class SessionOverviewViewModelTest { viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true)) togglePushNotificationUseCase.verifyExecute(A_SESSION_ID_1, true) - viewModel.test().assertLatestState { state -> state.notificationsEnabled }.finish() + viewModel.test().assertLatestState { state -> state.notificationsStatus == NotificationsStatus.ENABLED }.finish() } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt index 615330463b..c44fc4a497 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt @@ -28,8 +28,8 @@ import org.matrix.android.sdk.api.session.events.model.Content class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed = true) { - fun givenGetUserAccountDataEventReturns(type: String, content: Content) { - every { getUserAccountDataEvent(type) } returns UserAccountDataEvent(type, content) + fun givenGetUserAccountDataEventReturns(type: String, content: Content?) { + every { getUserAccountDataEvent(type) } returns content?.let { UserAccountDataEvent(type, it) } } fun givenUpdateUserAccountDataEventSucceeds() { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt index 92e311cfb7..bfbbb87705 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt @@ -16,7 +16,7 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.overview.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk From e67cc2b2dbb73a37dbadca599159fea8fa5adf0b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:03:11 +0200 Subject: [PATCH 054/679] Adding unit tests on GetNotificationsStatusUseCase --- .../GetNotificationsStatusUseCase.kt | 4 +- .../GetNotificationsStatusUseCaseTest.kt | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 1eb612988a..091338c46a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -34,11 +33,10 @@ class GetNotificationsStatusUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - // TODO add unit tests fun execute(deviceId: String): Flow { val session = activeSessionHolder.getSafeActiveSession() return when { - session == null -> emptyFlow() + session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { session.flow() .livePushers() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt new file mode 100644 index 0000000000..598c8df83f --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fixtures.PusherFixture +import im.vector.app.test.testDispatcher +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +private const val A_DEVICE_ID = "device-id" + +class GetNotificationsStatusUseCaseTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = + mockk() + + private val getNotificationsStatusUseCase = + GetNotificationsStatusUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `given NO current session when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + + @Test + fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + + @Test + fun `given current session and toggle via pusher is supported when execute then resulting flow contains status based on pusher value`() = runTest { + // Given + val pushers = listOf( + PusherFixture.aPusher( + deviceId = A_DEVICE_ID, + enabled = true, + ) + ) + fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + } + + @Test + fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = LocalNotificationSettingsContent( + isSilenced = false + ).toContent(), + ) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns true + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + } +} From 52a77e074f4d7503dc1a362b6e4b14d0c6f61cf2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:12:17 +0200 Subject: [PATCH 055/679] Renaming const for feature value --- .../org/matrix/android/sdk/internal/auth/version/Versions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index bc2d4a5aef..f4de6a9ae9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -57,7 +57,7 @@ private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" -private const val FEATURE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" +private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" /** * Return true if the SDK supports this homeserver version. @@ -151,5 +151,5 @@ private fun Versions.getMaxVersion(): HomeServerVersion { * @return true if remote toggle of push notifications is supported */ internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean { - return unstableFeatures?.get(FEATURE_PUSH_NOTIFICATIONS_MSC3881).orFalse() + return unstableFeatures?.get(FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881).orFalse() } From ac05e757beb8d40e4c41782d2230a5d8b6035ad9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:20:43 +0200 Subject: [PATCH 056/679] Small improvement to avoid tou many viewState updates --- .../devices/v2/notification/GetNotificationsStatusUseCase.kt | 3 +++ .../settings/devices/v2/overview/SessionOverviewViewModel.kt | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 091338c46a..313c1678cb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -43,6 +44,7 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { it.filter { pusher -> pusher.deviceId == deviceId } } .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() } checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { session.flow() @@ -50,6 +52,7 @@ class GetNotificationsStatusUseCase @Inject constructor( .unwrap() .map { it.content.toModel()?.isSilenced?.not() } .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() } else -> flowOf(NotificationsStatus.NOT_SUPPORTED) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 1aa5f676cc..e6aa7c2747 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -30,7 +30,6 @@ import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase -import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult @@ -220,9 +219,6 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { togglePushNotificationUseCase.execute(action.deviceId, action.enabled) - // TODO should not be needed => test without - val status = if (action.enabled) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED - setState { copy(notificationsStatus = status) } } } } From a851e5aa8504a6fce051b0e345606c505c35f080 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 28 Oct 2022 15:57:33 +0200 Subject: [PATCH 057/679] VoiceBroadcastPlayer - Add seek control views --- .../src/main/res/values/strings.xml | 2 + .../ui-styles/src/main/res/values/dimens.xml | 3 +- .../MessageVoiceBroadcastListeningItem.kt | 10 ++++ .../res/drawable/ic_player_backward_30.xml | 12 ++++ .../res/drawable/ic_player_forward_30.xml | 12 ++++ ...e_event_voice_broadcast_listening_stub.xml | 58 ++++++++++++++++--- ...e_event_voice_broadcast_recording_stub.xml | 8 +-- 7 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_player_backward_30.xml create mode 100644 vector/src/main/res/drawable/ic_player_forward_30.xml diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 2ea209a8f0..450eb64849 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3094,6 +3094,8 @@ Play or resume voice broadcast Pause voice broadcast Buffering + Fast backward 30 seconds + Fast forward 30 seconds Can’t start a new voice broadcast You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 50d5aaf014..22c2a3e62c 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -74,7 +74,8 @@ 22dp - 48dp + 48dp + 36dp 112dp diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 8df7a9d1a6..7a586ad411 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -18,6 +18,9 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageButton +import android.widget.SeekBar +import android.widget.TextView +import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -56,6 +59,9 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING + fastBackwardButton.isInvisible = true + fastForwardButton.isInvisible = true + when (state) { VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) @@ -85,6 +91,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) { val playPauseButton by bind(R.id.playPauseButton) val bufferingView by bind(R.id.bufferingView) + val fastBackwardButton by bind(R.id.fastBackwardButton) + val fastForwardButton by bind(R.id.fastForwardButton) + val seekBar by bind(R.id.seekBar) + val durationView by bind(R.id.playbackDuration) val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata) val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata) val listenersCountMetadata by bind(R.id.listenersCountMetadata) diff --git a/vector/src/main/res/drawable/ic_player_backward_30.xml b/vector/src/main/res/drawable/ic_player_backward_30.xml new file mode 100644 index 0000000000..cb244806b3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_player_backward_30.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_player_forward_30.xml b/vector/src/main/res/drawable/ic_player_forward_30.xml new file mode 100644 index 0000000000..be61fda8ff --- /dev/null +++ b/vector/src/main/res/drawable/ic_player_forward_30.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index d508569cb0..bed9407dfa 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -84,22 +84,31 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="bottom" - app:barrierMargin="12dp" + app:barrierMargin="10dp" app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" /> + + + + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml index 3296134919..7da0701cc7 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_recording_stub.xml @@ -91,8 +91,8 @@ Date: Mon, 31 Oct 2022 10:01:45 +0100 Subject: [PATCH 058/679] VoiceBroadcastPlayer - seek implementation --- .../home/room/detail/RoomDetailAction.kt | 3 +- .../home/room/detail/TimelineViewModel.kt | 20 +++--- .../factory/VoiceBroadcastItemFactory.kt | 1 + .../timeline/helper/TimelineEventsGroups.kt | 5 ++ .../item/AbsMessageVoiceBroadcastItem.kt | 1 + .../MessageVoiceBroadcastListeningItem.kt | 27 ++++++-- .../VoiceBroadcastExtensions.kt | 2 + .../voicebroadcast/VoiceBroadcastHelper.kt | 8 ++- .../listening/VoiceBroadcastPlayer.kt | 5 ++ .../listening/VoiceBroadcastPlayerImpl.kt | 62 +++++++++++++++---- 10 files changed, 107 insertions(+), 27 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index f773671694..8c49213a42 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -129,9 +129,10 @@ sealed class RoomDetailAction : VectorViewModelAction { } sealed class Listening : VoiceBroadcastAction() { - data class PlayOrResume(val eventId: String) : Listening() + data class PlayOrResume(val voiceBroadcastId: String) : Listening() object Pause : Listening() object Stop : Listening() + data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 50bebc81e4..3f4fae1ce9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -50,6 +50,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.error.RoomNotFound import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler @@ -478,7 +479,7 @@ class TimelineViewModel @AssistedInject constructor( is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.VoiceBroadcastAction -> handleVoiceBroadcastAction(action) + is VoiceBroadcastAction -> handleVoiceBroadcastAction(action) is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action) @@ -620,22 +621,23 @@ class TimelineViewModel @AssistedInject constructor( } } - private fun handleVoiceBroadcastAction(action: RoomDetailAction.VoiceBroadcastAction) { + private fun handleVoiceBroadcastAction(action: VoiceBroadcastAction) { if (room == null) return viewModelScope.launch { when (action) { - RoomDetailAction.VoiceBroadcastAction.Recording.Start -> { + VoiceBroadcastAction.Recording.Start -> { voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold( { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) }, { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) }, ) } - RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) - RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) - RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) - is RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.eventId) - RoomDetailAction.VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() - RoomDetailAction.VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() + VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) + VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) + VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) + is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.voiceBroadcastId) + VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() + VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() + is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 56498fa8d3..5d9c663210 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -67,6 +67,7 @@ class VoiceBroadcastItemFactory @Inject constructor( val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( voiceBroadcastId = voiceBroadcastId, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, + duration = voiceBroadcastEventsGroup.getDuration(), recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index 8a3be7d5f2..7738b6b680 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.utils.TextUtils import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState @@ -148,4 +149,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } ?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L } } + + fun getDuration(): Int { + return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.reduceOrNull { acc, duration -> acc + duration } ?: 0 + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index ba9d582ea4..7ada0c71f2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -94,6 +94,7 @@ abstract class AbsMessageVoiceBroadcastItem { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } + playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { - callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) - } + playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } } VoiceBroadcastPlayer.State.BUFFERING -> Unit } } } + private fun bindSeekBar(holder: Holder) { + holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration) + holder.seekBar.max = voiceBroadcastAttributes.duration + holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit + + override fun onStartTrackingTouch(seekBar: SeekBar) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar) { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress)) + } + }) + } + + private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) + override fun unbind(holder: Holder) { super.unbind(holder) player.removeListener(voiceBroadcastId, playerListener) + holder.seekBar.setOnSeekBarChangeListener(null) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index f9da2e76b1..48554f51d0 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -32,3 +32,5 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? { } val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence + +val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index dfc8e35422..7864d3b4e3 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -41,9 +41,15 @@ class VoiceBroadcastHelper @Inject constructor( suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId) - fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.playOrResume(roomId, eventId) + fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId) fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() + + fun seekTo(voiceBroadcastId: String, positionMillis: Int) { + if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) { + voiceBroadcastPlayer.seekTo(positionMillis) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index e2870c4011..2a2a549af0 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -43,6 +43,11 @@ interface VoiceBroadcastPlayer { */ fun stop() + /** + * Seek to the given playback position, is milliseconds. + */ + fun seekTo(positionMillis: Int) + /** * Add a [Listener] to the given voice broadcast id. */ diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 3999a0e0af..b0e5d93d1e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -22,7 +22,7 @@ import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure -import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk +import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase @@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import timber.log.Timber @@ -69,7 +70,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentSequence: Int? = null private var fetchPlaylistJob: Job? = null - private var playlist = emptyList() + private var playlist = emptyList() + private var isLive: Boolean = false override var currentVoiceBroadcastId: String? = null @@ -170,8 +172,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( .launchIn(coroutineScope) } - private fun updatePlaylist(playlist: List) { - this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } + private fun updatePlaylist(audioEvents: List) { + val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } + val chunkPositions = sorted + .map { it.duration } + .runningFold(0) { acc, i -> acc + i } + .dropLast(1) + this.playlist = sorted.mapIndexed { index, messageAudioEvent -> + PlaylistItem( + audioEvent = messageAudioEvent, + startTime = chunkPositions.getOrNull(index) ?: 0 + ) + } onPlaylistUpdated() } @@ -195,16 +207,23 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun startPlayback() { - val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() - val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = event.getVoiceBroadcastChunk()?.sequence + private fun startPlayback(sequence: Int? = null, position: Int = 0) { + val playlistItem = when { + sequence != null -> playlist.find { it.audioEvent.sequence == sequence } + isLive -> playlist.lastOrNull() + else -> playlist.firstOrNull() + } + val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } + val computedSequence = playlistItem.audioEvent.sequence coroutineScope.launch { try { currentMediaPlayer = prepareMediaPlayer(content) currentMediaPlayer?.start() + if (position > 0) { + currentMediaPlayer?.seekTo(position) + } currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } - currentSequence = sequence + currentSequence = computedSequence withContext(Dispatchers.Main) { playingState = State.PLAYING } nextMediaPlayer = prepareNextMediaPlayer() } catch (failure: Throwable) { @@ -220,11 +239,27 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.PLAYING } + override fun seekTo(positionMillis: Int) { + val duration = getVoiceBroadcastDuration() + val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return + val chunk = playlistItem.audioEvent + val chunkPosition = positionMillis - playlistItem.startTime + + Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${chunk.sequence}, sequencePosition=$chunkPosition") + + tryOrNull { currentMediaPlayer?.stop() } + release(currentMediaPlayer) + tryOrNull { nextMediaPlayer?.stop() } + release(nextMediaPlayer) + + startPlayback(chunk.sequence, chunkPosition) + } + private fun getNextAudioContent(): MessageAudioContent? { val nextSequence = currentSequence?.plus(1) - ?: playlist.lastOrNull()?.sequence + ?: playlist.lastOrNull()?.audioEvent?.sequence ?: 1 - return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content + return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content } private suspend fun prepareNextMediaPlayer(): MediaPlayer? { @@ -302,4 +337,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( return true } } + + private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 + + private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) } + From bc3fe4e5f68892cdedd3133283ce4fcde34b0bb4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 15:45:07 +0100 Subject: [PATCH 059/679] Minor cleanup --- .../listening/VoiceBroadcastPlayerImpl.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index b0e5d93d1e..e6d1c88b1a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -178,7 +178,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( .map { it.duration } .runningFold(0) { acc, i -> acc + i } .dropLast(1) - this.playlist = sorted.mapIndexed { index, messageAudioEvent -> + playlist = sorted.mapIndexed { index, messageAudioEvent -> PlaylistItem( audioEvent = messageAudioEvent, startTime = chunkPositions.getOrNull(index) ?: 0 @@ -242,17 +242,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun seekTo(positionMillis: Int) { val duration = getVoiceBroadcastDuration() val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return - val chunk = playlistItem.audioEvent - val chunkPosition = positionMillis - playlistItem.startTime + val audioEvent = playlistItem.audioEvent + val eventPosition = positionMillis - playlistItem.startTime - Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${chunk.sequence}, sequencePosition=$chunkPosition") + Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${audioEvent.sequence}, sequencePosition=$eventPosition") tryOrNull { currentMediaPlayer?.stop() } release(currentMediaPlayer) tryOrNull { nextMediaPlayer?.stop() } release(nextMediaPlayer) - startPlayback(chunk.sequence, chunkPosition) + startPlayback(audioEvent.sequence, eventPosition) } private fun getNextAudioContent(): MessageAudioContent? { @@ -342,4 +342,3 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) } - From 7d3f6365e28c77de9115b1b2777e149c53a2380f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 17:08:56 +0100 Subject: [PATCH 060/679] Use sum() instead of reduce operator --- .../home/room/detail/timeline/helper/TimelineEventsGroups.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt index 7738b6b680..a4bfa9e155 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -151,6 +151,6 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) { } fun getDuration(): Int { - return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.reduceOrNull { acc, duration -> acc + duration } ?: 0 + return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum() } } From eb61a23bf6bbafda36f2633d106497209e1f48c0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 18:42:26 +0100 Subject: [PATCH 061/679] Temporary disable seekBar if playing state is paused or idle --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 20d75a6d8d..a2d1e30c99 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -69,14 +69,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } + seekBar.isEnabled = true } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } + seekBar.isEnabled = false + } + VoiceBroadcastPlayer.State.BUFFERING -> { + seekBar.isEnabled = true } - VoiceBroadcastPlayer.State.BUFFERING -> Unit } } } From b41346cdcecf3a7750ba52d86b66b6b39bac7ff7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 18:42:52 +0100 Subject: [PATCH 062/679] Improve player transitions --- .../listening/VoiceBroadcastPlayerImpl.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index e6d1c88b1a..ee537b9e61 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -63,10 +63,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - set(value) { - field = value - currentMediaPlayer?.setNextMediaPlayer(value) - } private var currentSequence: Int? = null private var fetchPlaylistJob: Job? = null @@ -303,7 +299,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { @@ -317,6 +313,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor( return false } + override fun onPrepared(mp: MediaPlayer) { + when (mp) { + currentMediaPlayer -> { + nextMediaPlayer?.let { mp.setNextMediaPlayer(it) } + } + nextMediaPlayer -> { + tryOrNull { currentMediaPlayer?.setNextMediaPlayer(mp) } + } + } + } + override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return val roomId = currentRoomId ?: return From 481388ed329b33ee2deb3150e8ae5ddf46956200 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 19:01:41 +0100 Subject: [PATCH 063/679] Fix line length --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index ee537b9e61..166e5a12e5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -299,7 +299,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + private inner class MediaPlayerListener : + MediaPlayer.OnInfoListener, + MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { From 404383e683aa87b391601657afdd5edd9fd682e4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 2 Nov 2022 17:58:10 +0100 Subject: [PATCH 064/679] Update versions --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 968d8515ac..f50b672077 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.6\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.8\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 9a0bfdfa63..f793fff2c8 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 6 +ext.versionPatch = 8 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From bb02209537e3b4e04147ea43fb665717d4550d62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Nov 2022 23:10:39 +0000 Subject: [PATCH 065/679] Bump checker from 3.11.0 to 3.27.0 Bumps [checker](https://github.com/typetools/checker-framework) from 3.11.0 to 3.27.0. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.11.0...checker-framework-3.27.0) --- updated-dependencies: - dependency-name: org.checkerframework:checker dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 9857f88479..2f67a8eedb 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -308,7 +308,7 @@ dependencies { // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0) //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26. - implementation "org.checkerframework:checker:3.11.0" + implementation "org.checkerframework:checker:3.27.0" androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner From e9daef97b660ce8dda1db215517b982b52889ac5 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 11:27:02 +0100 Subject: [PATCH 066/679] Fix order of check to get notification status --- .../GetNotificationsStatusUseCase.kt | 16 ++++++++-------- .../GetNotificationsStatusUseCaseTest.kt | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 313c1678cb..69659bf23f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -38,14 +38,6 @@ class GetNotificationsStatusUseCase @Inject constructor( val session = activeSessionHolder.getSafeActiveSession() return when { session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) - checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { - session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } - .distinctUntilChanged() - } checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { session.flow() .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) @@ -54,6 +46,14 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } .distinctUntilChanged() } + checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() + } else -> flowOf(NotificationsStatus.NOT_SUPPORTED) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index 598c8df83f..b13018a20d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -22,6 +22,7 @@ import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.testDispatcher import io.mockk.every import io.mockk.mockk +import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.test.resetMain @@ -89,6 +90,11 @@ class GetNotificationsStatusUseCaseTest { // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + verifyOrder { + // we should first check account data + fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() + } } @Test From c0ba2f2f48f0b6c850aa8c29f508db3aba61b3c6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 3 Nov 2022 00:11:23 +0100 Subject: [PATCH 067/679] Fix bad content types when sending unencrypted media --- changelog.d/7519.bugfix | 1 + .../android/sdk/internal/session/content/UploadContentWorker.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7519.bugfix diff --git a/changelog.d/7519.bugfix b/changelog.d/7519.bugfix new file mode 100644 index 0000000000..c687bded49 --- /dev/null +++ b/changelog.d/7519.bugfix @@ -0,0 +1 @@ +Voice Broadcast - Fix error on voice messages in unencrypted rooms diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index db1cd1b33b..3dd440737a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -408,7 +408,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter newAttachmentAttributes: NewAttachmentAttributes ) { localEchoRepository.updateEcho(eventId) { _, event -> - val content: Content? = event.asDomain().content + val content: Content? = event.asDomain(castJsonNumbers = true).content val messageContent: MessageContent? = content.toModel() // Retrieve potential additional content from the original event val additionalContent = content.orEmpty() - messageContent?.toContent().orEmpty().keys From b0a31304a1ae0649c478884996013c70718486dc Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 17:04:49 +0100 Subject: [PATCH 068/679] Update seek bar tick progress while playing --- .../factory/VoiceBroadcastItemFactory.kt | 3 + .../item/AbsMessageVoiceBroadcastItem.kt | 4 + .../MessageVoiceBroadcastListeningItem.kt | 30 +++++++- .../listening/VoiceBroadcastPlayerImpl.kt | 74 +++++++++++++++++-- 4 files changed, 100 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 5d9c663210..06d3563303 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem @@ -44,6 +45,7 @@ class VoiceBroadcastItemFactory @Inject constructor( private val drawableProvider: DrawableProvider, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, + private val playbackTracker: AudioMessagePlaybackTracker, ) { fun create( @@ -71,6 +73,7 @@ class VoiceBroadcastItemFactory @Inject constructor( recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, + playbackTracker = playbackTracker, roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(), colorProvider = colorProvider, drawableProvider = drawableProvider, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 7ada0c71f2..9ea0a634c5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -25,6 +25,7 @@ import im.vector.app.R import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder @@ -40,6 +41,8 @@ abstract class AbsMessageVoiceBroadcastItem() { private lateinit var playerListener: VoiceBroadcastPlayer.Listener + private var isUserSeeking = false override fun bind(holder: Holder) { super.bind(holder) @@ -86,15 +88,36 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindSeekBar(holder: Holder) { - holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration) - holder.seekBar.max = voiceBroadcastAttributes.duration + holder.durationView.text = formatPlaybackTime(duration) + holder.seekBar.max = duration holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit - override fun onStartTrackingTouch(seekBar: SeekBar) = Unit + override fun onStartTrackingTouch(seekBar: SeekBar) { + isUserSeeking = true + } override fun onStopTrackingTouch(seekBar: SeekBar) { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress)) + isUserSeeking = false + } + }) + playbackTracker.track(voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { + override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Paused -> { + if (!isUserSeeking) { + holder.seekBar.progress = state.playbackTime + } + } + is AudioMessagePlaybackTracker.Listener.State.Playing -> { + if (!isUserSeeking) { + holder.seekBar.progress = state.playbackTime + } + } + AudioMessagePlaybackTracker.Listener.State.Idle -> Unit + is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit + } } }) } @@ -105,6 +128,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem super.unbind(holder) player.removeListener(voiceBroadcastId, playerListener) holder.seekBar.setOnSeekBarChangeListener(null) + playbackTracker.untrack(voiceBroadcastId) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 166e5a12e5..4fbaee8374 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -29,6 +29,7 @@ import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroad import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent @@ -60,6 +62,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var voiceBroadcastStateJob: Job? = null private val mediaPlayerListener = MediaPlayerListener() + private val playbackTicker = PlaybackTicker() private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null @@ -79,6 +82,24 @@ class VoiceBroadcastPlayerImpl @Inject constructor( field = value // Notify state change to all the listeners attached to the current voice broadcast id currentVoiceBroadcastId?.let { voiceBroadcastId -> + when (value) { + State.PLAYING -> { + playbackTracker.startPlayback(voiceBroadcastId) + playbackTicker.startPlaybackTicker(voiceBroadcastId) + } + State.PAUSED -> { + playbackTracker.pausePlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + State.BUFFERING -> { + playbackTracker.pausePlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + State.IDLE -> { + playbackTracker.stopPlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + } listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } } } @@ -99,15 +120,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun pause() { - currentMediaPlayer?.pause() - currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) } playingState = State.PAUSED + currentMediaPlayer?.pause() } override fun stop() { + // Update state + playingState = State.IDLE + // Stop playback currentMediaPlayer?.stop() - currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } isLive = false // Release current player @@ -126,9 +148,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( fetchPlaylistJob?.cancel() fetchPlaylistJob = null - // Update state - playingState = State.IDLE - // Clear playlist playlist = emptyList() currentSequence = null @@ -218,7 +237,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (position > 0) { currentMediaPlayer?.seekTo(position) } - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } currentSequence = computedSequence withContext(Dispatchers.Main) { playingState = State.PLAYING } nextMediaPlayer = prepareNextMediaPlayer() @@ -231,7 +249,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun resumePlayback() { currentMediaPlayer?.start() - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } playingState = State.PLAYING } @@ -352,4 +369,45 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) + + private inner class PlaybackTicker( + private var playbackTicker: CountUpTimer? = null, + ) { + + fun startPlaybackTicker(id: String) { + playbackTicker?.stop() + playbackTicker = CountUpTimer().apply { + tickListener = object : CountUpTimer.TickListener { + override fun onTick(milliseconds: Long) { + onPlaybackTick(id) + } + } + resume() + } + onPlaybackTick(id) + } + + private fun onPlaybackTick(id: String) { + if (currentMediaPlayer?.isPlaying.orFalse()) { + val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime + val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) + if (currentVoiceBroadcastPosition != null) { + val totalDuration = getVoiceBroadcastDuration() + val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration + playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) + } else { + playbackTracker.stopPlayback(id) + stopPlaybackTicker() + } + } else { + playbackTracker.stopPlayback(id) + stopPlaybackTicker() + } + } + + fun stopPlaybackTicker() { + playbackTicker?.stop() + playbackTicker = null + } + } } From 20d62b14dea44cacfa8c770a339bd37b3bcaaf14 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 1 Nov 2022 10:28:24 +0100 Subject: [PATCH 069/679] Changelog --- changelog.d/7496.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7496.wip diff --git a/changelog.d/7496.wip b/changelog.d/7496.wip new file mode 100644 index 0000000000..49d15d084f --- /dev/null +++ b/changelog.d/7496.wip @@ -0,0 +1 @@ +[Voice Broadcast] Add seekbar in listening tile From 6d850b30306936b5b4602b48242c3524b5dfb730 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 3 Nov 2022 16:17:55 +0100 Subject: [PATCH 070/679] Create VoiceBroadcast model with roomId and eventId --- .../home/room/detail/RoomDetailAction.kt | 5 +- .../home/room/detail/TimelineViewModel.kt | 4 +- .../factory/VoiceBroadcastItemFactory.kt | 9 ++-- .../item/AbsMessageVoiceBroadcastItem.kt | 5 +- .../MessageVoiceBroadcastListeningItem.kt | 12 ++--- .../voicebroadcast/VoiceBroadcastHelper.kt | 7 +-- .../listening/VoiceBroadcastPlayer.kt | 16 +++--- .../listening/VoiceBroadcastPlayerImpl.kt | 49 +++++++++---------- .../GetLiveVoiceBroadcastChunksUseCase.kt | 15 +++--- .../voicebroadcast/model/VoiceBroadcast.kt | 22 +++++++++ ...se.kt => GetVoiceBroadcastEventUseCase.kt} | 14 +++--- 11 files changed, 93 insertions(+), 65 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetVoiceBroadcastUseCase.kt => GetVoiceBroadcastEventUseCase.kt} (72%) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 8c49213a42..ba0f7dbdf8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.conference.ConferenceEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent @@ -129,10 +130,10 @@ sealed class RoomDetailAction : VectorViewModelAction { } sealed class Listening : VoiceBroadcastAction() { - data class PlayOrResume(val voiceBroadcastId: String) : Listening() + data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening() object Pause : Listening() object Stop : Listening() - data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening() + data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int) : Listening() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 3f4fae1ce9..252823b2a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -634,10 +634,10 @@ class TimelineViewModel @AssistedInject constructor( VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) - is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.voiceBroadcastId) + is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast) VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() - is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis) + is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 06d3563303..e4f7bed72f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -29,6 +29,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder @@ -60,14 +61,14 @@ class VoiceBroadcastItemFactory @Inject constructor( val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null - val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId + val voiceBroadcast = VoiceBroadcast(voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId, roomId = params.event.roomId) val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId && messageContent.deviceId == session.sessionParams.deviceId val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( - voiceBroadcastId = voiceBroadcastId, + voiceBroadcast = voiceBroadcast, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, duration = voiceBroadcastEventsGroup.getDuration(), recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), @@ -92,7 +93,7 @@ class VoiceBroadcastItemFactory @Inject constructor( voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastRecordingItem { return MessageVoiceBroadcastRecordingItem_() - .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}") .attributes(attributes) .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) @@ -105,7 +106,7 @@ class VoiceBroadcastItemFactory @Inject constructor( voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes, ): MessageVoiceBroadcastListeningItem { return MessageVoiceBroadcastListeningItem_() - .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}") + .id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}") .attributes(attributes) .voiceBroadcastAttributes(voiceBroadcastAttributes) .highlighted(highlight) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 9ea0a634c5..0329adf12b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -27,6 +27,7 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.util.MatrixItem @@ -36,7 +37,7 @@ abstract class AbsMessageVoiceBroadcastItem renderPlayingState(holder, state) } - player.addListener(voiceBroadcastId, playerListener) + player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) } @@ -77,7 +77,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) } + playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } seekBar.isEnabled = false } VoiceBroadcastPlayer.State.BUFFERING -> { @@ -98,11 +98,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } override fun onStopTrackingTouch(seekBar: SeekBar) { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress)) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress)) isUserSeeking = false } }) - playbackTracker.track(voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { + playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { when (state) { is AudioMessagePlaybackTracker.Listener.State.Paused -> { @@ -126,9 +126,9 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) - player.removeListener(voiceBroadcastId, playerListener) + player.removeListener(voiceBroadcast, playerListener) holder.seekBar.setOnSeekBarChangeListener(null) - playbackTracker.untrack(voiceBroadcastId) + playbackTracker.untrack(voiceBroadcast.voiceBroadcastId) } override fun getViewStubId() = STUB_ID diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 7864d3b4e3..6839056520 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -17,6 +17,7 @@ package im.vector.app.features.voicebroadcast import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase @@ -41,14 +42,14 @@ class VoiceBroadcastHelper @Inject constructor( suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId) - fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId) + fun playOrResumePlayback(voiceBroadcast: VoiceBroadcast) = voiceBroadcastPlayer.playOrResume(voiceBroadcast) fun pausePlayback() = voiceBroadcastPlayer.pause() fun stopPlayback() = voiceBroadcastPlayer.stop() - fun seekTo(voiceBroadcastId: String, positionMillis: Int) { - if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) { + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { + if (voiceBroadcastPlayer.currentVoiceBroadcast == voiceBroadcast) { voiceBroadcastPlayer.seekTo(positionMillis) } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 2a2a549af0..b4806ba57d 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -16,12 +16,14 @@ package im.vector.app.features.voicebroadcast.listening +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast + interface VoiceBroadcastPlayer { /** - * The current playing voice broadcast identifier, if any. + * The current playing voice broadcast, if any. */ - val currentVoiceBroadcastId: String? + val currentVoiceBroadcast: VoiceBroadcast? /** * The current playing [State], [State.IDLE] by default. @@ -31,7 +33,7 @@ interface VoiceBroadcastPlayer { /** * Start playback of the given voice broadcast. */ - fun playOrResume(roomId: String, voiceBroadcastId: String) + fun playOrResume(voiceBroadcast: VoiceBroadcast) /** * Pause playback of the current voice broadcast, if any. @@ -49,14 +51,14 @@ interface VoiceBroadcastPlayer { fun seekTo(positionMillis: Int) /** - * Add a [Listener] to the given voice broadcast id. + * Add a [Listener] to the given voice broadcast. */ - fun addListener(voiceBroadcastId: String, listener: Listener) + fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) /** - * Remove a [Listener] from the given voice broadcast id. + * Remove a [Listener] from the given voice broadcast. */ - fun removeListener(voiceBroadcastId: String, listener: Listener) + fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) /** * Player states. diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 4fbaee8374..fc983e4112 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -26,9 +26,10 @@ import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.sequence -import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -51,7 +52,7 @@ import javax.inject.Singleton class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, - private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase, private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { @@ -73,7 +74,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var isLive: Boolean = false - override var currentVoiceBroadcastId: String? = null + override var currentVoiceBroadcast: VoiceBroadcast? = null override var playingState = State.IDLE @MainThread @@ -81,7 +82,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( Timber.w("## VoiceBroadcastPlayer state: $field -> $value") field = value // Notify state change to all the listeners attached to the current voice broadcast id - currentVoiceBroadcastId?.let { voiceBroadcastId -> + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> when (value) { State.PLAYING -> { playbackTracker.startPlayback(voiceBroadcastId) @@ -103,17 +104,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } } } - private var currentRoomId: String? = null /** * Map voiceBroadcastId to listeners. */ private val listeners: MutableMap> = mutableMapOf() - override fun playOrResume(roomId: String, voiceBroadcastId: String) { - val hasChanged = currentVoiceBroadcastId != voiceBroadcastId + override fun playOrResume(voiceBroadcast: VoiceBroadcast) { + val hasChanged = currentVoiceBroadcast != voiceBroadcast when { - hasChanged -> startPlayback(roomId, voiceBroadcastId) + hasChanged -> startPlayback(voiceBroadcast) playingState == State.PAUSED -> resumePlayback() else -> Unit } @@ -152,37 +152,35 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playlist = emptyList() currentSequence = null - currentRoomId = null - currentVoiceBroadcastId = null + currentVoiceBroadcast = null } - override fun addListener(voiceBroadcastId: String, listener: Listener) { - listeners[voiceBroadcastId]?.add(listener) ?: run { - listeners[voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } + override fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { + listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run { + listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } - if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE) + listener.onStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) } - override fun removeListener(voiceBroadcastId: String, listener: Listener) { - listeners[voiceBroadcastId]?.remove(listener) + override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { + listeners[voiceBroadcast.voiceBroadcastId]?.remove(listener) } - private fun startPlayback(roomId: String, eventId: String) { + private fun startPlayback(voiceBroadcast: VoiceBroadcast) { // Stop listening previous voice broadcast if any if (playingState != State.IDLE) stop() - currentRoomId = roomId - currentVoiceBroadcastId = eventId + currentVoiceBroadcast = voiceBroadcast playingState = State.BUFFERING - val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState + val voiceBroadcastState = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content?.voiceBroadcastState isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED - fetchPlaylistAndStartPlayback(roomId, eventId) + fetchPlaylistAndStartPlayback(voiceBroadcast) } - private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) { - fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId) + private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { + fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) .onEach(this::updatePlaylist) .launchIn(coroutineScope) } @@ -347,9 +345,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - val roomId = currentRoomId ?: return - val voiceBroadcastId = currentVoiceBroadcastId ?: return - val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return + val voiceBroadcast = currentVoiceBroadcast ?: return + val voiceBroadcastEventContent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content ?: return isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 4f9f2de673..2e8fc31870 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -19,11 +19,12 @@ package im.vector.app.features.voicebroadcast.listening.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence -import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -44,19 +45,19 @@ import javax.inject.Inject */ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, + private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase, ) { - fun execute(roomId: String, voiceBroadcastId: String): Flow> { + fun execute(voiceBroadcast: VoiceBroadcast): Flow> { val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow() - val room = session.roomService().getRoom(roomId) ?: return emptyFlow() + val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow() val timeline = room.timelineService().createTimeline(null, TimelineSettings(5)) // Get initial chunks - val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId) + val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } - val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId) + val voiceBroadcastEvent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) { @@ -82,7 +83,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( lastSequence = stopEvent.content?.lastChunkSequence } - val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId) + val newChunks = newEvents.mapToChunkEvents(voiceBroadcast.voiceBroadcastId, voiceBroadcastEvent.root.senderId) // Notify about new chunks if (newChunks.isNotEmpty()) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt new file mode 100644 index 0000000000..62207d5b87 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.model + +data class VoiceBroadcast( + val voiceBroadcastId: String, + val roomId: String, +) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt similarity index 72% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index d08fa14a95..26ba3209b7 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -16,6 +16,7 @@ package im.vector.app.features.voicebroadcast.usecase +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import org.matrix.android.sdk.api.session.Session @@ -24,17 +25,18 @@ import org.matrix.android.sdk.api.session.getRoom import timber.log.Timber import javax.inject.Inject -class GetVoiceBroadcastUseCase @Inject constructor( +class GetVoiceBroadcastEventUseCase @Inject constructor( private val session: Session, ) { - fun execute(roomId: String, eventId: String): VoiceBroadcastEvent? { - val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") + fun execute(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { + val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") - Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $eventId") + Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast") - val initialEvent = room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() // Fallback to initial event - val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId).sortedBy { it.root.originServerTs } + val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent() + val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) + .sortedBy { it.root.originServerTs } return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent } } From e2327eaf79d1ae402d2b90934a6f47321e92add1 Mon Sep 17 00:00:00 2001 From: Mubark Date: Thu, 3 Nov 2022 10:23:40 +0000 Subject: [PATCH 071/679] Translated using Weblate (Arabic) Currently translated at 39.5% (1001 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ar/ --- library/ui-strings/src/main/res/values-ar/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-ar/strings.xml b/library/ui-strings/src/main/res/values-ar/strings.xml index 70b9a33ab5..a49ecc3d08 100644 --- a/library/ui-strings/src/main/res/values-ar/strings.xml +++ b/library/ui-strings/src/main/res/values-ar/strings.xml @@ -1167,4 +1167,12 @@ البريد الإلكتروني كلمة السر الجديدة التالي - + + صفر + واحد + اثنان + قليلة + كثيرة + اخرى + + \ No newline at end of file From 7b8274710853ff64ee46bc7912c23c41cd8b15b9 Mon Sep 17 00:00:00 2001 From: Nizami Date: Thu, 3 Nov 2022 13:29:06 +0000 Subject: [PATCH 072/679] Translated using Weblate (Azerbaijani) Currently translated at 4.8% (123 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/az/ --- library/ui-strings/src/main/res/values-az/strings.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/library/ui-strings/src/main/res/values-az/strings.xml b/library/ui-strings/src/main/res/values-az/strings.xml index 044ecf900c..6fe322bdd0 100644 --- a/library/ui-strings/src/main/res/values-az/strings.xml +++ b/library/ui-strings/src/main/res/values-az/strings.xml @@ -20,7 +20,7 @@ %s səsli zəng etdi. %s zəngə cavab verdi. %s zəng başa çatdı. - "%1$s gələcək otaq tarixçəsini %2$s-ə görünən etdi" + %1$s gələcək otaq tarixçəsini %2$s-ə görünən etdi bütün otaq üzvləri, dəvət olunduğu andan. bütün otaq üzvləri, qoşulduğu andan. bütün otaq üzvləri. @@ -48,8 +48,9 @@ \nKriptografiyanın idxalı İlkin sinxronizasiya: \nOtaqlar idxalı - İlkin sinxronizasiya: -\nOtaqlara daxil olmaq + İlkin sinxronizasiya: +\nSöhbətləriniz yüklənilir +\nƏgər çoxlu otaqlara qoşulmusunuzsa, bu, bir az vaxt apara bilər İlkin sinxronizasiya: \nDəvət olunmuş otaqların idxalı İlkin sinxronizasiya: @@ -133,4 +134,6 @@ Otağa qoşulmaq üçün %1$s-a dəvət göndərdiniz %s, bu otaq üçün server ACL-lərini dəyişdi. • %s ilə uyğunlaşan serverlərə icazə verildi. + Siz %1$s üçün otağa qoşulmaq dəvətin ləğv etdiniz + %1$s-ı dəvət etdiniz \ No newline at end of file From d6819dd8d72d94c5da7a577ae08a71938ab78649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20V=C3=A1gner?= Date: Wed, 2 Nov 2022 14:17:06 +0000 Subject: [PATCH 073/679] Translated using Weblate (Slovak) Currently translated at 100.0% (2531 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index bf57233b37..a41aca05dc 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2653,7 +2653,7 @@ V záujme čo najlepšieho zabezpečenia, overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate. Iné relácie Relácie - Otvoriť zoznam priestorov + Zoznam priestorov Vytvoriť novú konverzáciu alebo miestnosť Ľudia Obľúbené From ec22278eed3fa341cd2f824e4ab5f504b11b2439 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 2 Nov 2022 22:11:12 +0000 Subject: [PATCH 074/679] Translated using Weblate (Albanian) Currently translated at 98.8% (2501 of 2531 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/ --- .../src/main/res/values-sq/strings.xml | 418 ++++++++++++++++-- 1 file changed, 388 insertions(+), 30 deletions(-) diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index a6af0a4921..b1d8eb9564 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -601,9 +601,7 @@ Formatojini mesazhet duke përdorur sintaksën Markdown përpara se të dërgohen. Kjo lejon formatim të thelluar, f.v., përdorimi i yllthit për ta shfaqur tekstin me të pjerrëta. Nuk prek ftesat, heqjet dhe dëbimet. ${app_name}-i grumbullon të dhëna analitike anonime që të na lejojë ta përmirësojmë aplikacionin. - Të shfaqen krejt mesazhet prej %s\? -\n -\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. + Të shfaqen krejt mesazhet prej %s\? Nis kamerën e sistemit, në vend se skenën e kamerës vetjake. Shfaq veprimin On/Off sintakse Markdown @@ -897,10 +895,10 @@ S’u arrit të dërgohej sugjerimi (%s) Shfaq te rrjedha kohore akte të fshehura Përgjegjës integrimesh - app_id: - push_key: - app_display_name: - emër_sesioni: + ID Aplikacioni: + + Emër Aplikacioni Në Ekran: + Emër Sesioni Në Ekran: Mesazhe të Drejtpërdrejtë Po pritet… Po fshehtëzohet miniatura… @@ -949,11 +947,11 @@ Po përdorni %1$s për të zbuluar dhe për të qenë i zbulueshëm nga kontakte ekzistues që njihni. S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jini i zbulueshëm nga kontakte ekzistuese që njihni, formësoni një të tillë më poshtë. Adresa email të zbulueshme - Mundësitë rreth zbulimesh do të shfaqen sapo të keni shtuar një email. + Mundësitë e zbulimit do të shfaqen sapo të keni shtuar një adresë email. Mundësi zbulimesh do të shfaqen sapo të keni shtuar një numër telefoni. Shkëputja prej shërbyesit tuaj të identiteteve do të thotë se s’do të jeni i zbulueshëm prej përdoruesish të tjerë dhe s’do të jeni në gjendje të ftoni të tjerë me email ose telefon. Numra telefoni të zbulueshëm - Ju dërguam një email ripohimi te %s, hapeni dhe klikoni mbi lidhjen e ripohimit + Ju dërguam një email te %s, hapeni dhe klikoni mbi lidhjen e ripohimit Jepni një URL shërbyesi identitetesh S’u lidh dot te shërbyes identitetesh Ju lutemi, jepni URL-në e shërbyesit të identiteteve @@ -1080,7 +1078,7 @@ Aplikacioni s’është në gjendje të krijojë llogari në këtë shërbyes Home. \n \nDoni të regjistroheni duke përdorur një klient web\? - Ky emai s’është përshoqëruar me ndonjë llogari. + Kjo adresë email s’është e përshoqëruar me ndonjë llogari. Ricaktoni fjalëkalimin në %1$s Te mesazhet tuaj do të dërgohet një email verifikimi, për të ripohuar caktimin e fjalëkalimit tuaj të ri. Pasuesi @@ -1089,7 +1087,7 @@ Kujdes! Ndryshimi i fjalëkalimit tuaj do të sjellë zerim të çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt sesionet tuaj, duke e bërë të palexueshëm historikun e bisedave të fshehtëzuara. Ujdisni një Kopjeruajtje Kyçesh ose eksportoni kyçet e dhomës tuaj prej një tjetër sesioni, përpara se të ricaktoni fjalëkalimin tuaj. Vazhdo - Ky email s’është i lidhur me ndonjë llogari + Kjo adresë email s’është e lidhur me ndonjë llogari Kontrolloni te mesazhet tuaj të marrë Një email verifikimi u dërgua te %1$s. Prekni mbi lidhjen që të ripohohet fjalëkalimi juaj i ri. Pasi të keni ndjekur lidhjen që përmban, klikoni më poshtë. @@ -1103,7 +1101,7 @@ \n \nTë ndalet procesi i ndryshimit të fjalëkalimit\? Caktoni adresë email - Caktoni një email për rimarrje të llogarisë tuaj. Më vonë, mundeni të lejoni persona që njihni t’ju zbulojnë përmes email-it tuaj. + Caktoni një adresë email për rimarrje të llogarisë tuaj. Më vonë, mundeni të lejoni persona që njihni t’ju zbulojnë përmes kësaj adrese. Email Email (në daçi) Pasuesi @@ -1277,7 +1275,7 @@ \n - Shërbyesi Home te i cili është lidhur përdoruesi që po verifikoni \n - Lidhja juaj internet ose ajo e përdoruesit tjetër \n - Pajisja juaj ose ajo e përdoruesit tjetër - %s u anulua + %s u pranua Skanojeni kodin me pajisjen e përdoruesit tjetër, për të verifikuar në mënyrë të sigurt njëri-tjetrin Nëse s’jeni vetë atje, krahasoni emoji-n @@ -1445,7 +1443,7 @@ Mesazhi u fshi Shfaq mesazhe të hequr Shfaq një vendmbajtëse për mesazhe të hequr - Ju dërguam një email ripohimi te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit + Ju dërguam një email te %s, ju lutemi, së pari, shihni email-in tuaj dhe klikoni mbi lidhjen e ripohimit Kodi i verifikimit s’është i saktë. MEDIA S’ka media në këtë dhomë @@ -1518,9 +1516,7 @@ \n \nKëtë veprim mund ta zhbëni në çfarëdo kohe, te rregullimet e përgjithshme. Hiqe shpërfilljen e përdoruesit - Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. -\n -\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe do të hajë ca kohë. + Heqja e shpërfilljes së këtij përdoruesi do të shfaqë sërish krejt mesazhet prej tij. Anuloje ftesën Jeni i sigurt se doni të anulohet ftesa për këtë përdorues\? Përzëre përdoruesin @@ -1534,7 +1530,7 @@ Heqja e dëbimit përdoruesit do t’i lejojë të marrë pjesë sërish në dhomë. Te llogaria juaj s’është shtuar ndonjë numër telefoni Adresa email - Te llogaria juaj s’është shtuar ndonjë email + Te llogaria juaj s’është shtuar ndonjë adresë email Numra telefoni Të hiqet %s\? Sigurohuni që keni klikuar te lidhja në email-in që ju kemi dërguar. @@ -1552,7 +1548,7 @@ Integrimet janë të çaktivizuara Që të bëhet kjo, aktivizoni “Lejo integrime”, te Rregullimet. Email-e dhe numra telefonash - Administroni email-e dhe numra telefonash të lidhur me llogarinë tuaj Matrix + Administroni adresa email dhe numra telefonash të lidhur me llogarinë tuaj Matrix %d përdorues i dëbuar %d përdorues të dëbuar @@ -1605,7 +1601,7 @@ Kjo llogari është çaktivizuar. S’u ruajt dot kartelë media Ripohoni identitetin tuaj duke verifikuar këto kredenciale hyrjeje, duke i akorduar hyrje te mesazhe të fshehtëzuar. - Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim email-esh dhe numrash telefoni përdoruesi të koduar. + Për privatësinë tuaj, ${app_name}-i mbulon vetëm dërgim adresash email dhe numrash telefoni përdoruesi të koduar. Caktoni rol Rol Hapni fjalosje @@ -1769,7 +1765,7 @@ %1$d nga %2$d Jepe pranimin Shfuqizoje pranimin tim - Keni dhënë pranimin tuaj për të dërguar email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj. + Keni dhënë pranimin tuaj për të dërguar adresa email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj. Dërgo email-e dhe numra telefonash Sugjerime Përdorues të Ditur @@ -2135,7 +2131,7 @@ Përmendje dhe Fjalëkyçe Njoftime Parazgjedhje %s te Rregullimet, që të merrni ftesa drejt e në ${app_name}. - Lidheni këtë email me llogarinë tuaj + Lidheni këtë adresë email me llogarinë tuaj Kjo ftesë për te kjo hapësirë u dërgua te %s që s’është i përshoqëruar me llogarinë tuaj Kjo ftesë për te kjo dhomë qe dërguar për %s që s’është i përshoqëruar me llogarinë tuaj Krejt dhomat ku gjendeni do të shfaqen te Home. @@ -2203,7 +2199,7 @@ Hyrje në hapësirë Kush mund të hyjë\? Aktivizo njoftime me email për %s - Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një email + Që të merrni email me njoftim, ju lutemi, përshoqërojini llogarisë tuaj Matrix një adresë email Njoftim me email Të përmirësojë hapësirën Të ndryshojë emrin e hapësirës @@ -2249,8 +2245,8 @@ Pyetje ose temë pyetësori Krijoni Pyetësor A pranoni të dërgohen këto hollësi\? - Për të zbuluar kontakte ekzistuese, duhet të dërgoni hollësi kontakti (email-e dhe numra telefonash) te shërbyesi juaj i identiteteve. Para dërgimit, i fshehtëzojmë të dhënat tuaja, për privatësi. - Dërgo email-e dhe numra telefonash te %s + Për të zbuluar kontakte ekzistuese, duhet të dërgoni hollësi kontakti (adresa email dhe numra telefonash) te shërbyesi juaj i identiteteve. Para dërgimit, i fshehtëzojmë të dhënat tuaja, për privatësi. + Dërgo adresa email dhe numra telefonash te %s Kontaktet tuaja janë private. Për të zbuluar përdorues prej kontakteve tuaja, na duhet leja juaj për të dërguar hollësi kontakti te shërbyesi juaj i identiteteve. Është bërë dalja nga sesioni! U dol nga dhoma! @@ -2355,7 +2351,7 @@ Bashkësi Ekipe Shokë dhe familje - Do t’ju ndihmojmë të lidheni. + Do t’ju ndihmojmë të lidheni Me kë do të bisedoni më shumë\? Po e shihni tashmë këtë rrjedhë! Shiheni në Dhomë @@ -2411,15 +2407,15 @@ Shërbyesi Home s’pranon emër përdorues vetëm me shifra. Anashkalojeni këtë hap Ruajeni dhe vazhdoni - Parapëlqimet tuaja u ruajtën. + Kaloni te rregullimet, kur të doni, që të përditësoni profilin tuaj Kaq qe! Shkojmë - Këtë mund ta ndryshoni kurdo. + Erdh koha t’i jepet surrat emrit Shtoni një foto profili Këtë mund ta ndryshoni më vonë Emër Në Ekran Zgjidhni një emër për në ekran - Llogaria juaj %s u krijua. + Llogaria juaj %s u krijua Përgëzime! Shpjemëni në shtëpi Personalizoni profil @@ -2450,4 +2446,366 @@ Prani Mësoni më tepër Provojeni - + Aktivizo shkurtore lejesh për Thirrje Element + S’u gjet metodë tjetër veç njëkohësimit në prapaskenë. + ${app_name}-it i duhet një fshehtinë e pastër, për të qenë i përditësuar, për arsyen vijuese: +\n%s +\n +\nKini parasysh se ky veprim do të sjellë rinisjen e aplikacionit dhe mund të dojë ca kohë. + Regjistro emrin, versionin dhe URL-në e klientit, për të dalluar më kollaj sesionet te përgjegjës sesionesh. + Veprimtaria e fundit më %1$s + Apliko format me të nënvizuara + Apliko format me të hequravije + Apliko format me të pjerrta + Apliko format me të trasha + Ju lutemi, sigurohuni se e dini origjinën e këtij kodi. Duke lidhur pajisje, do t’i jepni dikujt hyrje të plotë në llogarinë tuaj. + Ripohojeni + Riprovoni + Pa përputhje\? + Po bëhet hyrja juaj + Po lidhet me pajisjen + Skanoni kodin QR + Po bëhet hyrja te një pajisje celulare\? + Shfaq kod QR te kjo pajisje + Përzgjidhni “Skanoni kod QR” + Filloja në skenën e hyrjes + Përzgjidhni “Hyni me kod QR” + Filloja në skenën e hyrjes + Përzgjidhni “Shfaq kod QR” + Kaloni te Rregullime -> Siguri & Privatësi + Hapeni aplikacionin në pajisjen tuaj tjetër + Hyrja u anulua në pajisjen tuaj tjetër. + Ai kod QR është i pavlefshëm. + Duhet bërë hyrja te pajisja tjetër. + Nga pajisja tjetër është bërë tashmë hyrja. + Kërkesa dështoi. + Kërkesa u hodh poshtë në pajisjen tjetër. + Lidhja me këtë pajisje nuk mbulohet. + Lidhje e pasuksesshme + U vendos lidhje e siguruar + Hyni me kod QR + Skanoni kodin QR + 3 + 2 + 1 + Provojeni + Prekeni djathtas në krye që të shihni mundësinë për dhënie përshtypjesh. + Jepni Përshtypje + Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë. + Hyni Në Hapësira + Që të thjeshtohet ${app_name} juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye. + Mirë se vini te një pamje e re! + Ky është vendi ku do të shfaqen mesazhet tuaj të palexuar, kur të ketë të tillë. + S’ka gjë për ta raportuar. + Aplikacioni “all-in-one” i fjalosjeve të siguruara, për ekipe, shokë dhe ente. Që t’ia filloni, krijoni një fjalosje, ose hyni në një dhomë ekzistuese. + Mirë se vini te ${app_name}, +\n%s. + Hapësirat janë një mënyrë e re për të grupuar dhoma dhe persona. Shtoni një dhomë ekzistuese, ose krijoni një të re, duke përdorur butonin poshtë djathtas. + %s +\nduket paksa si i zbrazët. + Jini në gjendje të incizoni dhe dërgoni transmetim zanor në rrjedhën kohore të dhomës. + Aktivizoni transmetim zanor (nën zhvillim aktiv) + Aktivizo regjistrim hollësish klienti + Shihini më qartë dhe kontrolloni më mirë krejt sesionet tuaj. + Aktivizo përgjegjës të ri sesionesh + Përdorues të tjerë në mesazhe të drejtpërdrejtë dhe dhoma ku hyni janë në gjendje të shohin një listë të plotë të sesioneve tuaj. +\n +\nKjo u jep atyre besim se po flasin vërtet me ju, por do të thotë gjithashtu që mund shohin emrin e sesionit që jepni këtu. + Riemërtim sesionesh + Sesionet e verifikuar përfaqësojnë sesione ku është bërë hyrja dhe janë verifikuar, ose duke përdorur togfjalëshin tuaj të sigurt, ose me verifikim. +\n +\nKjo do të thotë se zotërojnë kyçe fshehtëzimi për mesazhe tuajt të mëparshëm dhe u ripohojnë përdoruesve të tjerë, me të cilët po komunikoni, se këto sesione ju takojnë juve. + Sesione të verifikuar + Sesionet e paverifikuar janë sesione në të cilët është bërë hyrja me kredencialet tuaja, por pa u bërë verifikim. +\n +\nDuhet të jeni posaçërisht të qartë se i njihni këto sesione, ngaqë mund të përbëjnë përdorim të paautorizuar të llogarisë tuaj. + Sesione të paverifikuar + Sesioni joaktive janë sesione që keni ca kohë që s’i përdorni, por që vazhdojnë të marrin kyçe fshehtëzimi. +\n +\nHeqja e sesioneve joaktive përmirëson sigurinë dhe punimin dhe e bën më të lehtë për ju të pikasni nëse një sesion i ri është i dyshimtë. + Sesione joaktive + Mund të përdorni këtë pajisje për të bërë hyrjen në një pajisje celulare apo web me një kod QR. Për ta bërë këtë ka dy mënyra: + Hyni me Kod QR + Ju lutemi, kini parasysh se emrat e sesioneve janë të dukshëm edhe për personat me të cilët komunikoni. + Emra vetjakë sesionesh mund t’ju ndihmojnë të njihni më kollaj pajisjet tuaja. + Emër sesioni + Riemërtoni sesionin + Adresë IP + Sistem operativ + Model + Shfletues + URL + Version + Ëmër + Aplikacion + Veprimtaria e fundit + Emër sesioni + Merrni njoftime push për këtë sesion. + Njoftime Push + Hollësi aplikacioni, pajisjeje dhe veprimtarie. + Hollësi sesioni + Dilni nga ky sesion + Përzgjidhni sesione + Spastroje Filtrin + S’u gjetën sesione joaktive. + S’u gjetën seanca të paverifikuara. + S’u gjetën sesione të verifikuara. + + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + + Joaktive + Verifikoni sesionet tuaj, për shkëmbim më të sigurt mesazhesh, ose dilni prej atyre që nuk i njihni, apo përdorni më. + Të paverifikuar + Për sigurinë më të mirë, dilni nga çfarëdo sesioni që nuk e njihni apo përdorni më. + Të verifikuar + Filtroji + + Joaktiv për %1$d ditë, ose më gjatë + Joaktiv për %1$d ditë, ose më gjatë + + Jo aktiv + Jo gati për shkëmbim të sigurt mesazhesh + E paverifikuar + Gati për shkëmbim të sigurt mesazhesh + E verifikuar + Krejt sesionet + Filtroji + Pajisje + Sesion + Sesioni i Tanishëm + + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + Shihni mundësinë e daljes nga sesione të vjetër (%1$d ditë ose më tepër) të cilët s’i përdorni më. + + Sesione joaktive + Verifikojini, ose dilni nga sesione të paverifikuar. + Sesione të paverifikuar + Përmirësoni sigurinë e llogarisë tuaj duke ndjekur këto rekomandime. + Rekomandime sigurie + + Joaktiv për %1$d+ ditë (%2$s) + Joaktiv për %1$d+ ditë (%2$s) + + I paverifikuar · Sesioni juaj i tanishëm + I paverifikuar · Veprimtari së fundi më %1$s + I verifikuar · Veprimtaria e fundit më %1$s + Shihni Krejt (%1$d) + Shihni Hollësitë + Verifiko Sesion + Verifikoni sesionin tuaj të tanishëm, që të shfaqni gjendjen e verifikimit të këtij sesioni. + Për sigurinë dhe besueshmërinë më të mirë, verifikojeni, ose dilni nga ky sesion. + Verifikoni sesionin tuaj të tanishëm, për shkëmbim më të sigurt të mesazheve. + Ky sesion është gati për shkëmbim të sigurt mesazhesh. + Sesioni juaj i tanishëm është gati për shkëmbim të sigurt mesazhesh. + Gjendje e panjohur verifikimi + Sesion i paverifikuar + Sesion i verifikuar + Lloj i panjohur pajisjeje + Desktop + Web + Celular + Për sigurinë më të mirë, verifikoni sesionet tuaja dhe dilni nga çfarëdo sesioni që s’e njihni, ose s’e përdorni më. + Sesione të tjera + + U hoq %d mesazh + U hoqë %d mesazhe + + Aktivizoni tregim vendndodhjeje + Ju lutemi, kini parasysh: kjo është një veçori në zhvillim, që përdor një sendërtim të përkohshëm. Kjo do të thotë se s’do të jeni në gjendje të fshini historikun e vendndodhjeve tuaja dhe përdoruesit e përparuar do të jenë në gjendje të shohin historikun e vendndodhjeve tuaja, edhe pasi të keni ndalur dhënien “live” për këtë dhomë të vendndodhjes tuaj. + Tregim “live” vendndodhjeje + Kanal i tanishëm: %s + Kanal + S’gjendet pikëmbarimi. + Pikëmbarim i tanishëm: %s + Pikëmbarim + Hëpërhë po përdoret %s. + Metodë + + U gjet %d metodë. + U gjetën %d metoda. + + S’u gjet metodë tjetër veç Google Play Service. + Metoda të gatshme + Metodë njoftimi + Njëkohësim në prapaskenë + Shërbime Google + Zgjidhni si të merren njoftime + Tregimi i ekranit është në punë e sipër + Tregim Ekrani ${app_name} + Kontakt + Kamerë + Vendndodhje + Pyetësorë + Transmetim zanor + Bashkëngjitje + Ngjitës + Fototekë + Nisni një transmetim zanor + Vendndodhje drejtpërsëdrejti + Jepe vendndodhjen + Që të mund të ndani drejtpërsëdrejti vendndodhje me të tjerë në këtë dhomë, lypset të keni lejet e duhura. + S’keni leje të tregoni vendndodhje drejtpërsëdrejti + Përditësuar %1$s më parë + Sendërtim i përkohshëm: vendndodhjet mbeten në historikun e dhomës + Aktivizo Tregim Vendndodhjeje “Live” + Vendndodhje Drejtpërsëdrejti ${app_name} + Edhe %1$s + “Live” deri më %1$s + Shihni vendndodhje “live” + Tregimi “live” i vendndodhjes përfundoi + Po ngarkohet vendndodhje “live”… + S’arrihet të ngarkohet hartë +\nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta. + Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori + Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm. + Përdo + Ndal transmetim zanor + Luani ose vazhdoni luajtje transmetimi zanor + Ndal incizim transmetimi zanor + Ndal incizim transmetimi zanor + Vazhdo incizim transmetimi zanor + Drejtpërdrejt + Shfaq hollësitë më të reja të përdoruesit + Disa përfundime mund të jenë të fshehura, ngaqë janë private dhe ju duhet një ftesë për to. + S’u gjetën përfundime + Mos braktis ndonjë + Braktisi krejt + Gjëra në këtë hapësirë + I zënë + Hap rregullimet + S’u aktivizua dot mirëfilltësim biometrik. + Mirëfilltësimi biometrik qe çaktivizuar ngaqë tani së fundi është shtuar një metodë e re mirëfilltësimi biometrik. Mund ta riaktivizoni që nga Rregullimet. + S’mund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje. + Tastierë inkonjito + Dërgoni mesazhin tuaj të parë për të ftuar në fjalosje %s + Mesazhet në këtë fjalosje do të jenë të fshehtëzuar skaj-më-skaj. + S’do të jeni në gjendje të shihni historikun e mesazheve të fshehtëzuara. Që t’ia rifilloni nga e para, ricaktoni kyçet tuaja për Kopjeruajtje të Sigurt Mesazhesh dhe kyçe verifikimi. + S’arrihet të verifikohet kjo pajisje + Sesione + Tregoi vendndodhjen e vet drejtpërsëdrejti + E paraprin një mesazh tekst i thjeshtë me (╯°□°)╯︵ ┻━┻ + S’hapet dot kjo lidhje: bashkësitë janë zëvendësuar nga hapësirat + Skanoni kodin QR + Emër përdoruesi / Email / Telefon + Jeni qenie njerëzore\? + Ndiqni udhëzimet e dërguara te %s + Ricaktim fjalëkalimi + Harrova fjalëkalimin + Ridërgo email + S’morët email\? + Ndiqni udhëzimet e dërguara te %s + Verifikoni email-in tuaj + Ridërgomëni kodin + Te %s u dërgua një kod + Ripohoni numrin e telefonit tuaj + Dil nga krejt pajisjet + Ricaktoni fjalëkalimin + Sigurohuni të jetë 8 ose më shumë shenja. + Zgjidhni një fjalëkalim të ri + Fjalëkalim i Ri + Kontrolloni email-in tuaj. + %s do t’ju dërgojë një lidhje verifikimi + Kod ripohimi + Numër Telefoni + %s lyp verifikimin e llogarisë tuaj + Jepni numrin e telefonit tuaj + Email + %s lyp verifikimin e llogarisë tuaj + Jepni email-in tuaj + Ju lutemi, lexoni kushte dhe rregulla të %s + Rregulla shërbyesi + Lidhuni + Element Matrix Services (EMS) është një shërbim strehimi i fuqishëm dhe i besueshëm, për komunikim të shpejtë, të sigurt dhe të atypëratyshëm. Shihni më tepër se si, teelement.io/ems + Doni të strehoni shërbyesin tuaj\? + URL Shërbyesi + Cila është adresa e shërbyesit tuaj\? + Cila është adresa e shërbyesit tuaj\? Kjo është si një shtëpi për krejt të dhënat tuaja + Përzgjidhni shërbyesin tuaj + Mirë se u kthyet! + Përpunojeni + Ose + Ku gjenden bisedat tuaja + Ku do të gjenden bisedat tuaja + Duhet të jetë 8 ose më shumë shenja + Të tjerët mund t’ju zbulojnë %s + Krijoni llogarinë tuaj + Transmetim Zanor + Hap listë hapësirash + Krijoni një bisedë ose dhomë të re + Ricaktoni metodë njoftimesh + Të aktivizuara: + Etiketë profili: + ID sesioni: + Jepi + Po përditësohen të dhënat tuaja… + Diç shkoi ters. Ju lutemi, kontrolloni lidhjen tuaj në rrjet dhe riprovoni. + Persona + Të parapëlqyera + Të palexuara + Krejt + Kopjeruajtja ka një nënshkrim të vlefshëm prej këtij përdoruesi. + Hap skenën e mjeteve të zhvilluesit + Na ndjeni, kjo dhomë s’u gjet. +\nJu lutemi, riprovoni më vonë.%s + Përdor parazgjedhje sistemi + Zgjidheni dorazi + Caktoje vetvetiu + Zgjidhni madhësi shkronjash + ⚠ Në këtë dhomë ka pajisje të paverifikuara, ato s’do të jenë në gjendje të shfshehtëzojnë mesazhet që dërgoni. + Mos dërgo kurrë prej këtij sesioni mesazhe të fshehtëzuar te sesione të paverifikuar në këtë dhomë. + Figurat e animuara vetëluaji + S’u arrit të regjistrohej token pikëmbarimi te shërbyesi Home: +\n%1$s + Pikëmbarim i regjistruar me sukses te shërbyesi Home. + Regjistrim Pikëmbarimi + Akordojini Leje + ${app_name} lyp lejen për shfaqje njoftimesh. +\nJu lutemi, akordoni lejen. + + %1$s dhe %2$d tjetër + %1$s dhe %2$d të tjerë + + %1$s dhe %2$s + ${app_name} lyp leje të shfaqë njoftime. Njoftimet mund të shfaqin mesazhet tuaja, ftesa tuajat, etj. +\n +\nJu lutemi, lejoni përdorimin e tyre te flluska pasuese, që të jeni në gjendje të shihni njoftime. + Email jo i verifikuar, kontrolloni te Të marrët tuaj + Reshtni tregimin e ekranit tuaj + Tregojuani ekranin të tjerëve + Ky është vendi ku do të gjenden kërkesat dhe ftesat tuaja të reja. + S’ka gjë të re. + Ftesa + Hapësirat janë një mënyrë e re për të grupuar dhoma dhe njerëz. Që t’ia filloni, krijoni një hapësirë. + Ende pa hapësira. + Provoni përpunuesin e teksteve të pasur (për tekst të thjeshtë vjen së shpejti) + Aktivizo përpunues teksti të pasur + Krijo MD vetëm për mesazhin e parë + Një Element i thjeshtuar, me skeda opsionale + Aktivizo skemë të re + A - Z + Veprimtari + Renditi sipas + Shfaq të freskëta + Shfaq filtra + Parapëlqime skeme grafike + Shpërzgjidhi krejt + Përzgjidhi krejt + E mora + Më pas + Rifillo + sek + min + h + - Për disa përdorues u hoq shpërfillja + Kërkesë njëkohësimi fillestar + Eksploroni Dhoma + Ndërroni Hapësire + Krijo Dhomë + Filloni Fjalosje + Krejt Fjalosjet + + %1$d i përzgjedhura + %1$d të përzgjedhura + + \ No newline at end of file From de56e08cd2499d41b8d9ded1ff826c490a00fc3d Mon Sep 17 00:00:00 2001 From: PotLice Date: Thu, 3 Nov 2022 03:37:20 +0000 Subject: [PATCH 075/679] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2519 of 2519 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hans/ --- .../src/main/res/values-zh-rCN/strings.xml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 688652265b..112b900da7 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -30,7 +30,7 @@ 发送者的设备没有向我们发送此消息的密钥。 无法发送消息 Matrix 错误 - 电子邮箱地址 + 电子邮件地址 手机号码 %1$s 撤回了对 %2$s 的邀请 %1$s 让未来的房间历史记录对 %2$s 可见 @@ -214,8 +214,8 @@ 登录 提交 错误的用户名和/或密码 - 此电子邮箱地址似乎无效 - 此电子邮箱地址已被使用。 + 此电子邮件地址似乎无效 + 此电子邮件地址已被使用。 忘记密码? 请输入有效的 URL 没有包含有效的 JSON @@ -228,7 +228,7 @@ 搜索 过滤房间成员 没有结果 - 添加电子邮箱地址 + 添加电子邮件地址 添加手机号码 版本 olm 版本 @@ -257,7 +257,7 @@ 开始视频通话 拍摄照片或视频 此主服务器想确认你不是机器人 - 电子邮箱地址验证失败:请确保你已点击邮件中的链接 + 电子邮件地址验证失败:请确保你已点击邮件中的链接 原始 通话正在连接…… ${app_name} 需要权限以访问你的麦克风来进行语音通话。 @@ -351,8 +351,8 @@ 其他 通知目标 登录为 - 请检查你的电子邮箱并点击里面包含的链接。完成时请点击继续。 - 此电子邮箱地址已被使用。 + 请检查你的电子邮件并点击里面包含的链接。完成时请点击继续。 + 此电子邮件地址已被使用。 此手机号码已被使用。 设置为主要地址 取消设置为主要地址 @@ -828,7 +828,7 @@ 撤消 断开连接 拒绝 - 这不是有效的Matrix服务器地址 + 这不是有效的 Matrix 服务器地址 无法在此 URL 找到主服务器,请检查 播放 忽略 @@ -990,13 +990,13 @@ 更改身份服务器 你正在使用 %1$s 与你知道的现有联系人相互发现。 你当前未使用身份服务器。若要与你知道的现有联系人相互发现,请在下方配置。 - 可发现电子邮件地址 + 可发现的电子邮件地址 发现选项将在你添加电子邮件地址后出现。 发现选项将在你添加电话号码后出现。 - 与你的身份服务器断开意味着你将无法被其它用户发现并且无法通过电子邮件和电话邀请他人。 + 与您的身份服务器断开连接意味着您将不会被其他用户发现,并且您将无法通过电子邮件或电话邀请其他人。 可发现电话号码 我们向%s发送了一封电子邮件,请检查你的电子邮件并点击确认链接 - 我们向%s发送了电子邮件,请先检查你的电子邮件并点击确认链接 + 我们向 %s 发送了一封电子邮件,请先检查您的电子邮件并点击确认链接 输入身份服务器 URL 无法连接到身份服务器 请输入身份服务器 url @@ -1143,14 +1143,14 @@ 输入验证码 重新发送 下一个 - 国际电话号码必须以 ‘+’ 开头 + 国际电话号码必须以“+”开头 电话号码似乎无效。请检查 在 %1$s 上注册 用户名或电子邮件 用户名 密码 下一个 - 用户名已占用 + 该用户名已被使用 警告 你的账户尚未创建。是否中止注册过程? 选择 matrix.org @@ -1171,7 +1171,7 @@ 如果你在主服务器上设置了账户,在下方使用你的 Matrix ID(例 @user:domain.com)和密码。 Matrix ID 如果你不知道你的密码,返回并重置。 - 这不是一个有效的用户标识符。期望的格式:\'@user:homeserver.org\' + 这不是有效的用户标识符。预期格式:\'@user:homeserver.org\' 无法找到有效的主服务器。请检查你的标识符 你已登出 这可能由于多种原因: @@ -1199,7 +1199,7 @@ 除非你登录以恢复加密密钥,否则你将无法访问安全消息。 当前会话用于用户 %1$s 而你提供了用户 %2$s 的凭证。${app_name} 不支持此功能。 \n请先清除数据,然后重新登录另一个账户。 - 你的 matrix.to 链接更是不正确 + 您的 matrix.to 链接格式错误 描述太短 初始同步… 高级设置 @@ -1531,7 +1531,7 @@ 你无法访问此消息因为发送者有意不发送密钥 正在等待加密历史 Riot 现已成为 Element! - 我们很高兴地宣布我们改名了!你的应用已经更新到最新版本,并且你已登录你的账户。 + 我们很高兴地宣布我们已经更名了!您的应用程序是最新的,并且您已登录到您的帐户。 明白了 了解更多 将恢复密钥保存到 @@ -1588,9 +1588,9 @@ 移除 %s? 请确认你已点击我们向你发送的电子邮件中的链接。 电子邮件和电话号码 - 管理链接到你的Matrix账户的电子邮件地址和电话号码 + 管理与您的 Matrix 帐户链接的电子邮件地址和电话号码 代码 - 请使用国际格式(电话号码必须以“+”开始) + 请使用国际格式(电话号码必须以“+”开头) 验证此登录来确认你的身份,授权其访问加密消息。 无法打开你被封禁的房间。 无法找到此房间。请确认它存在。 @@ -1804,7 +1804,7 @@ %d 个条目 - 不是有效的 Matrix 二维码 + 这不是有效的 Matrix 二维码 扫描二维码 添加人员 邀请朋友 @@ -2105,9 +2105,9 @@ 可用视频通话 可用语音通话 在 ${app_name} 中直接接收邀请的设置 %s。 - 将此电子邮件地址与您的帐户相关联 - 加入这个空间的邀请被发送至 %s,此邮箱未与您的账户相关联 - 加入这个房间的邀请被发送至 %s,此邮箱未与您的账户相关联 + 将此电子邮件地址与您的帐户链接 + 此空间的邀请已发送至与您的帐户无关的 %s + 此房间的邀请已发送至与您的帐户无关的 %s 你所在的全部房间将显示在主页上。 在主页上显示所有房间 滑动结束通话 @@ -2171,7 +2171,7 @@ 空间访问 谁可以访问? 为 %s 启用电子邮件通知 - 要接收通知邮件,请将一个电子邮件地址关联到你的Matrix账户 + 要接收带有通知的电子邮件,请将电子邮件地址链接到您的 Matrix 帐户 电子邮件通知 升级空间 更改空间名称 @@ -2409,7 +2409,7 @@ 发送图片和视频 打开相机 服务器政策 - Element Matrix Services(EMS)是一个健壮且可靠的主机托管服务,可实现快速、安全和实时的通信。在<a href=\"${ftue_ems_url}\">element.io/ems</a>上了解如何使用 + Element Matrix Services (EMS) 是一种强大且可靠的托管服务,可实现快速、安全和实时的通信。 了解如何在 <a href=\"${ftue_ems_url}\">element.io/ems</a> 想架设自己的服务器? 服务器URL 选择你的服务器 @@ -2545,7 +2545,7 @@ 你的服务器地址是什么? 你的对话发生的地方 %1$s 和 %2$s - 电子邮件未确认,检查你的收件箱 + 电子邮件未验证,请检查您的收件箱 无法加载地图 \n此主服务器可能没有设置好显示地图。 打开设置 From 97841e117ddc2733e73d177fc3e1f0da05bab346 Mon Sep 17 00:00:00 2001 From: Nizami Date: Thu, 3 Nov 2022 13:35:54 +0000 Subject: [PATCH 076/679] Translated using Weblate (Azerbaijani) Currently translated at 2.5% (2 of 79 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/az/ --- fastlane/metadata/android/az/short_description.txt | 1 + fastlane/metadata/android/az/title.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/az/short_description.txt create mode 100644 fastlane/metadata/android/az/title.txt diff --git a/fastlane/metadata/android/az/short_description.txt b/fastlane/metadata/android/az/short_description.txt new file mode 100644 index 0000000000..ecf3d5008c --- /dev/null +++ b/fastlane/metadata/android/az/short_description.txt @@ -0,0 +1 @@ +Qrup mesajlaşma - şifrəli mesajlaşma, qrup söhbəti və video zənglər diff --git a/fastlane/metadata/android/az/title.txt b/fastlane/metadata/android/az/title.txt new file mode 100644 index 0000000000..4ca0ffb55b --- /dev/null +++ b/fastlane/metadata/android/az/title.txt @@ -0,0 +1 @@ +Element - Təhlükəsiz Mesajlaşma From 46260b57685ae1c6aa44672139dcd598601a8ef8 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 7 Nov 2022 00:04:35 +0000 Subject: [PATCH 077/679] Sync analytics plan --- .../features/analytics/plan/UserProperties.kt | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt index 28732c9a42..01720453ce 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt @@ -24,26 +24,6 @@ package im.vector.app.features.analytics.plan * definition. These properties must all be device independent. */ data class UserProperties( - /** - * Whether the user has the favourites space enabled. - */ - val webMetaSpaceFavouritesEnabled: Boolean? = null, - /** - * Whether the user has the home space set to all rooms. - */ - val webMetaSpaceHomeAllRooms: Boolean? = null, - /** - * Whether the user has the home space enabled. - */ - val webMetaSpaceHomeEnabled: Boolean? = null, - /** - * Whether the user has the other rooms space enabled. - */ - val webMetaSpaceOrphansEnabled: Boolean? = null, - /** - * Whether the user has the people space enabled. - */ - val webMetaSpacePeopleEnabled: Boolean? = null, /** * The active filter in the All Chats screen. */ @@ -109,11 +89,6 @@ data class UserProperties( fun getProperties(): Map? { return mutableMapOf().apply { - webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) } - webMetaSpaceHomeAllRooms?.let { put("WebMetaSpaceHomeAllRooms", it) } - webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) } - webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) } - webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) } allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) } ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } numFavouriteRooms?.let { put("numFavouriteRooms", it) } From 97cfc7dde47ec3fd00e1cde434ebe707048550a4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 09:37:12 +0200 Subject: [PATCH 078/679] Adding changelog entry --- changelog.d/7418.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7418.feature diff --git a/changelog.d/7418.feature b/changelog.d/7418.feature new file mode 100644 index 0000000000..b68ef700da --- /dev/null +++ b/changelog.d/7418.feature @@ -0,0 +1 @@ +[Session manager] Multi-session signout From f45cc715d16f1ae1b70953e561b47ebb0c4e2c79 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 10:13:34 +0200 Subject: [PATCH 079/679] Adding new menu entry for multi signout --- .../src/main/res/values/strings.xml | 2 ++ .../v2/othersessions/OtherSessionsFragment.kt | 20 +++++++++++++++++++ .../src/main/res/menu/menu_other_sessions.xml | 5 +++++ 3 files changed, 27 insertions(+) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 450eb64849..da62e4c300 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3345,6 +3345,8 @@ No inactive sessions found. Clear Filter Select sessions + Sign out of these sessions + Sign out Sign out of this session Session details Application, device, and activity information. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 4f1c8353f5..7737caa689 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.annotation.StringRes +import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -32,12 +33,14 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.orEmpty import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet @@ -77,9 +80,26 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() + val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) + multiSignoutItem.title = if (isSelectModeEnabled) { + getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() + } else { + getString(R.string.device_manager_other_sessions_multi_signout_all) + } + val showAsActionFlag = if (isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) + changeTextColorOfDestructiveAction(multiSignoutItem) } } + private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { + val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) + val currentTitle = menuItem.title.orEmpty().toString() + menuItem.title = currentTitle + .toSpannable() + .colorizeMatchingText(currentTitle, titleColor) + } + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.otherSessionsSelect -> { diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index 8339286fe7..d4a75bd0df 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -9,6 +9,11 @@ android:title="@string/device_manager_other_sessions_select" app:showAsAction="withText|never" /> + + Date: Thu, 20 Oct 2022 16:22:29 +0200 Subject: [PATCH 080/679] Adding overflow menu capability in sessions list header view --- .../stylable_sessions_list_header_view.xml | 1 + .../vector/app/core/extensions/MenuItemExt.kt | 29 +++++++++++++++++++ .../v2/VectorSettingsDevicesFragment.kt | 7 +++++ .../devices/v2/list/SessionsListHeaderView.kt | 16 ++++++++++ .../v2/othersessions/OtherSessionsFragment.kt | 9 ++---- .../res/layout/fragment_settings_devices.xml | 3 +- .../res/layout/view_sessions_list_header.xml | 11 ++++++- .../res/menu/menu_other_sessions_header.xml | 12 ++++++++ 8 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt create mode 100644 vector/src/main/res/menu/menu_other_sessions_header.xml diff --git a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml index 098ec263fc..c1a51000b7 100644 --- a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml +++ b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml @@ -5,6 +5,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt new file mode 100644 index 0000000000..7d62a0c357 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.extensions + +import android.view.MenuItem +import androidx.annotation.ColorInt +import androidx.core.text.toSpannable +import im.vector.app.core.utils.colorizeMatchingText + +fun MenuItem.setTextColor(@ColorInt color: Int) { + val currentTitle = title.orEmpty().toString() + title = currentTitle + .toSpannable() + .colorizeMatchingText(currentTitle, color) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 1c348af4f9..d192eef778 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -30,6 +30,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.dialogs.ManuallyVerifyDialog +import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider @@ -91,6 +92,7 @@ class VectorSettingsDevicesFragment : super.onViewCreated(view, savedInstanceState) initWaitingView() + initOtherSessionsHeaderView() initOtherSessionsView() initSecurityRecommendationsView() initQrLoginView() @@ -131,6 +133,11 @@ class VectorSettingsDevicesFragment : views.waitingView.waitingStatusText.isVisible = true } + private fun initOtherSessionsHeaderView() { + val color = colorProvider.getColorFromAttribute(R.attr.colorError) + views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) + } + private fun initOtherSessionsView() { views.deviceListOtherSessions.callback = this } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index 0660e7d642..51408931c7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -20,6 +20,9 @@ import android.content.Context import android.content.res.TypedArray import android.util.AttributeSet import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import androidx.appcompat.view.menu.MenuBuilder import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible @@ -39,6 +42,7 @@ class SessionsListHeaderView @JvmOverloads constructor( this ) + val menu: Menu = binding.sessionsListHeaderMenu.menu var onLearnMoreClickListener: (() -> Unit)? = null init { @@ -50,6 +54,7 @@ class SessionsListHeaderView @JvmOverloads constructor( ).use { setTitle(it) setDescription(it) + setMenu(it) } } @@ -90,4 +95,15 @@ class SessionsListHeaderView @JvmOverloads constructor( onLearnMoreClickListener?.invoke() } } + + private fun setMenu(typedArray: TypedArray) { + val menuResId = typedArray.getResourceId(R.styleable.SessionsListHeaderView_sessionsListHeaderMenu, -1) + if (menuResId == -1) { + binding.sessionsListHeaderMenu.isVisible = false + } else { + binding.sessionsListHeaderMenu.showOverflowMenu() + val menuBuilder = binding.sessionsListHeaderMenu.menu as? MenuBuilder + menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 7737caa689..2bed0c943b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -25,7 +25,6 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.annotation.StringRes -import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -33,14 +32,13 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.extensions.orEmpty +import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet @@ -94,10 +92,7 @@ class OtherSessionsFragment : private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) - val currentTitle = menuItem.title.orEmpty().toString() - menuItem.title = currentTitle - .toSpannable() - .colorizeMatchingText(currentTitle, titleColor) + menuItem.setTextColor(titleColor) } override fun handleMenuItemSelected(item: MenuItem): Boolean { diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml index 38137b2029..8134774887 100644 --- a/vector/src/main/res/layout/fragment_settings_devices.xml +++ b/vector/src/main/res/layout/fragment_settings_devices.xml @@ -98,6 +98,7 @@ app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession" app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description" app:sessionsListHeaderHasLearnMoreLink="false" + app:sessionsListHeaderMenu="@menu/menu_other_sessions_header" app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title" /> diff --git a/vector/src/main/res/layout/view_sessions_list_header.xml b/vector/src/main/res/layout/view_sessions_list_header.xml index 6139ff4815..9f581a1d03 100644 --- a/vector/src/main/res/layout/view_sessions_list_header.xml +++ b/vector/src/main/res/layout/view_sessions_list_header.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/layout_horizontal_margin" android:layout_marginTop="20dp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/sessionsListHeaderMenu" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Other sessions" /> @@ -29,4 +29,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title" tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." /> + + diff --git a/vector/src/main/res/menu/menu_other_sessions_header.xml b/vector/src/main/res/menu/menu_other_sessions_header.xml new file mode 100644 index 0000000000..4ab0b7465c --- /dev/null +++ b/vector/src/main/res/menu/menu_other_sessions_header.xml @@ -0,0 +1,12 @@ + + + + + + From ae4a7283581bacb5b8c9b860d65505adad956538 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 16:45:31 +0200 Subject: [PATCH 081/679] Handling press on multi signout action in other sessions list screen --- .../v2/othersessions/OtherSessionsAction.kt | 1 + .../v2/othersessions/OtherSessionsFragment.kt | 31 +++++++++++++------ .../othersessions/OtherSessionsViewModel.kt | 6 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index 1978708ebf..33bc8b3f4f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -26,4 +26,5 @@ sealed class OtherSessionsAction : VectorViewModelAction { data class ToggleSelectionForDevice(val deviceId: String) : OtherSessionsAction() object SelectAll : OtherSessionsAction() object DeselectAll : OtherSessionsAction() + object MultiSignout : OtherSessionsAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 2bed0c943b..8059a75c12 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -78,18 +78,27 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() - val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) - multiSignoutItem.title = if (isSelectModeEnabled) { - getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() - } else { - getString(R.string.device_manager_other_sessions_multi_signout_all) - } - val showAsActionFlag = if (isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER - multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) - changeTextColorOfDestructiveAction(multiSignoutItem) + updateMultiSignoutMenuItem(menu, state) } } + private fun updateMultiSignoutMenuItem(menu: Menu, viewState: OtherSessionsViewState) { + val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) + multiSignoutItem.title = if (viewState.isSelectModeEnabled) { + getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() + } else { + getString(R.string.device_manager_other_sessions_multi_signout_all) + } + multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) { + viewState.devices.invoke()?.any { it.isSelected }.orFalse() + } else { + viewState.devices.invoke()?.isNotEmpty().orFalse() + } + val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) + changeTextColorOfDestructiveAction(multiSignoutItem) + } + private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) menuItem.setTextColor(titleColor) @@ -109,6 +118,10 @@ class OtherSessionsFragment : viewModel.handle(OtherSessionsAction.DeselectAll) true } + R.id.otherSessionsMultiSignout -> { + viewModel.handle(OtherSessionsAction.MultiSignout) + true + } else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index 2cd0c6af66..cac5ce7d3b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -65,6 +65,7 @@ class OtherSessionsViewModel @AssistedInject constructor( } } + // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) @@ -73,6 +74,7 @@ class OtherSessionsViewModel @AssistedInject constructor( is OtherSessionsAction.ToggleSelectionForDevice -> handleToggleSelectionForDevice(action.deviceId) OtherSessionsAction.DeselectAll -> handleDeselectAll() OtherSessionsAction.SelectAll -> handleSelectAll() + OtherSessionsAction.MultiSignout -> handleMultiSignout() } } @@ -142,4 +144,8 @@ class OtherSessionsViewModel @AssistedInject constructor( ) } } + + private fun handleMultiSignout() { + // TODO call multi signout use case with all or only selected devices depending on the ViewState + } } From 810c93cef9f440efb4c88fa7956b14a1e9f46e1d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 17:41:47 +0200 Subject: [PATCH 082/679] Handling press on multi signout action from header menu in other sessions section --- .../features/settings/devices/v2/DevicesAction.kt | 1 + .../features/settings/devices/v2/DevicesViewModel.kt | 6 ++++++ .../devices/v2/VectorSettingsDevicesFragment.kt | 12 ++++++++++++ .../devices/v2/list/SessionsListHeaderView.kt | 5 +++++ 4 files changed, 24 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index c7437db44c..9ecb72a25c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -22,4 +22,5 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() + object MultiSignoutOtherSessions : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index a5405756eb..8f12bf28b6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -95,10 +95,12 @@ class DevicesViewModel @AssistedInject constructor( } } + // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() + DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() } } @@ -116,4 +118,8 @@ class DevicesViewModel @AssistedInject constructor( private fun handleMarkAsManuallyVerifiedAction() { // TODO implement when needed } + + private fun handleMultiSignoutOtherSessions() { + // TODO call multi signout use case with all other devices than the current one + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index d192eef778..f3de06a324 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -19,9 +19,11 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context import android.os.Bundle import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -48,6 +50,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsAction import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -136,6 +139,15 @@ class VectorSettingsDevicesFragment : private fun initOtherSessionsHeaderView() { val color = colorProvider.getColorFromAttribute(R.attr.colorError) views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) + views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> + when(menuItem.itemId) { + R.id.otherSessionsHeaderMultiSignout -> { + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + true + } + else -> false + } + } } private fun initOtherSessionsView() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index 51408931c7..f74d88790c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -23,6 +23,7 @@ import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible @@ -106,4 +107,8 @@ class SessionsListHeaderView @JvmOverloads constructor( menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) } } } + + fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) { + binding.sessionsListHeaderMenu.setOnMenuItemClickListener(listener) + } } From 7e836c0e97d4551d61a33a1e50efab4526c7bae3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 13:59:09 +0200 Subject: [PATCH 083/679] Updating the action title to include sessions number --- library/ui-strings/src/main/res/values/strings.xml | 4 ++++ .../devices/v2/VectorSettingsDevicesFragment.kt | 12 ++++++------ .../v2/othersessions/OtherSessionsFragment.kt | 3 ++- vector/src/main/res/menu/menu_other_sessions.xml | 2 +- .../src/main/res/menu/menu_other_sessions_header.xml | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index da62e4c300..e772748a41 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3347,6 +3347,10 @@ Select sessions Sign out of these sessions Sign out + + Sign out of %1$d session + Sign out of %1$d sessions + Sign out of this session Session details Application, device, and activity information. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index f3de06a324..e9778e1368 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -19,11 +19,9 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -50,7 +48,6 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState -import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsAction import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -137,10 +134,8 @@ class VectorSettingsDevicesFragment : } private fun initOtherSessionsHeaderView() { - val color = colorProvider.getColorFromAttribute(R.attr.colorError) - views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> - when(menuItem.itemId) { + when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { viewModel.handle(DevicesAction.MultiSignoutOtherSessions) true @@ -290,6 +285,11 @@ class VectorSettingsDevicesFragment : hideOtherSessionsView() } else { views.deviceListHeaderOtherSessions.isVisible = true + val color = colorProvider.getColorFromAttribute(R.attr.colorError) + val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout) + val nbDevices = otherDevices.size + multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) + multiSignoutItem.setTextColor(color) views.deviceListOtherSessions.isVisible = true views.deviceListOtherSessions.render( devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 8059a75c12..0429c3bbb3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -87,7 +87,8 @@ class OtherSessionsFragment : multiSignoutItem.title = if (viewState.isSelectModeEnabled) { getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() } else { - getString(R.string.device_manager_other_sessions_multi_signout_all) + val nbDevices = viewState.devices()?.size ?: 0 + stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) } multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) { viewState.devices.invoke()?.any { it.isSelected }.orFalse() diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index d4a75bd0df..7893575dde 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -11,7 +11,7 @@ From bb262f0c4164af94234d2f4157c9e0e99177b57c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 16:12:32 +0200 Subject: [PATCH 084/679] Adding new "delete_devices" request API --- .../sdk/api/session/crypto/CryptoService.kt | 3 ++ .../internal/crypto/DefaultCryptoService.kt | 8 +++- .../sdk/internal/crypto/api/CryptoApi.kt | 12 ++++++ .../crypto/model/rest/DeleteDeviceParams.kt | 5 ++- .../crypto/model/rest/DeleteDevicesParams.kt | 37 +++++++++++++++++++ .../internal/crypto/tasks/DeleteDeviceTask.kt | 21 ++++++++++- .../sdk/internal/network/NetworkConstants.kt | 1 + 7 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index d2aa8020e8..971d04261e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.crypto import android.content.Context +import androidx.annotation.Size import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback @@ -55,6 +56,8 @@ interface CryptoService { fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + fun deleteDevices(@Size(min = 1) deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + fun getCryptoVersion(context: Context, longFormat: Boolean): String fun isCryptoEnabled(): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 9c3e0ba1c5..032d649421 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -241,9 +241,15 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } + // TODO add unit test override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { + deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback) + } + + // TODO add unit test + override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) { + .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) { this.executionThread = TaskThread.CRYPTO this.callback = callback } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt index d5a8bdfd7c..cfe4681bfd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.api import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse @@ -136,6 +137,17 @@ internal interface CryptoApi { @Body params: DeleteDeviceParams ) + /** + * Deletes the given devices, and invalidates any access token associated with them. + * Doc: https://spec.matrix.org/v1.4/client-server-api/#post_matrixclientv3delete_devices + * + * @param params the deletion parameters + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_V3 + "delete_devices") + suspend fun deleteDevices( + @Body params: DeleteDevicesParams + ) + /** * Update the device information. * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt index c26c6107c4..24dccc4d90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -23,6 +23,9 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class DeleteDeviceParams( + /** + * Additional authentication information for the user-interactive authentication API. + */ @Json(name = "auth") - val auth: Map? = null + val auth: Map? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt new file mode 100644 index 0000000000..19b33b2a69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class provides the parameter to delete several devices. + */ +@JsonClass(generateAdapter = true) +internal data class DeleteDevicesParams( + /** + * Additional authentication information for the user-interactive authentication API. + */ + @Json(name = "auth") + val auth: Map? = null, + + /** + * Required: The list of device IDs to delete. + */ + @Json(name = "devices") + val deviceIds: List, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 0a77d33acc..fc6bc9b1bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.uia.UiaResult import org.matrix.android.sdk.internal.auth.registration.handleUIA import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -30,21 +31,37 @@ import javax.inject.Inject internal interface DeleteDeviceTask : Task { data class Params( - val deviceId: String, + val deviceIds: List, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, val userAuthParam: UIABaseAuth? ) } +// TODO add unit tests internal class DefaultDeleteDeviceTask @Inject constructor( private val cryptoApi: CryptoApi, private val globalErrorReceiver: GlobalErrorReceiver ) : DeleteDeviceTask { override suspend fun execute(params: DeleteDeviceTask.Params) { + require(params.deviceIds.isNotEmpty()) + try { executeRequest(globalErrorReceiver) { - cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) + val userAuthParam = params.userAuthParam?.asMap() + if (params.deviceIds.size == 1) { + cryptoApi.deleteDevice( + deviceId = params.deviceIds.first(), + DeleteDeviceParams(auth = userAuthParam) + ) + } else { + cryptoApi.deleteDevices( + DeleteDevicesParams( + auth = userAuthParam, + deviceIds = params.deviceIds + ) + ) + } } } catch (throwable: Throwable) { if (params.userInteractiveAuthInterceptor == null || diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt index 5aec7db66c..4bfda0bf3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -22,6 +22,7 @@ internal object NetworkConstants { const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/" + const val URI_API_PREFIX_PATH_V3 = "$URI_API_PREFIX_PATH/v3/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" // Media From 1bda54323a586818ae155d68cefd08947e5f80bb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 16:51:22 +0200 Subject: [PATCH 085/679] Calling signout multi sessions use case in other sessions screen --- .../v2/othersessions/OtherSessionsAction.kt | 6 ++ .../v2/othersessions/OtherSessionsFragment.kt | 51 +++++++++- .../othersessions/OtherSessionsViewEvents.kt | 10 +- .../othersessions/OtherSessionsViewModel.kt | 99 ++++++++++++++++++- .../othersessions/OtherSessionsViewState.kt | 1 + .../v2/signout/SignoutSessionUseCase.kt | 3 + .../v2/signout/SignoutSessionsUseCase.kt | 43 ++++++++ 7 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index 33bc8b3f4f..24d2a08bdc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType sealed class OtherSessionsAction : VectorViewModelAction { + // ReAuth + object SsoAuthDone : OtherSessionsAction() + data class PasswordAuthDone(val password: String) : OtherSessionsAction() + object ReAuthCancelled : OtherSessionsAction() + + // Others data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction() data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction() object DisableSelectMode : OtherSessionsAction() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 0429c3bbb3..ca9334ad08 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.othersessions +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -32,6 +33,7 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK @@ -40,6 +42,7 @@ import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.databinding.FragmentOtherSessionsBinding +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType @@ -47,6 +50,7 @@ import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import im.vector.app.features.themes.ThemeUtils +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject @@ -158,8 +162,9 @@ class OtherSessionsFragment : private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is OtherSessionsViewEvents.Loading -> showLoading(it.message) - is OtherSessionsViewEvents.Failure -> showFailure(it.throwable) + is OtherSessionsViewEvents.SignoutError -> showFailure(it.error) + is OtherSessionsViewEvents.RequestReAuth -> askForReAuthentication(it) + OtherSessionsViewEvents.SignoutSuccess -> enableSelectMode(false) } } } @@ -191,6 +196,7 @@ class OtherSessionsFragment : } override fun invalidate() = withState(viewModel) { state -> + updateLoading(state.isLoading) if (state.devices is Success) { val devices = state.devices.invoke() renderDevices(devices, state.currentFilter) @@ -198,6 +204,14 @@ class OtherSessionsFragment : } } + private fun updateLoading(isLoading: Boolean) { + if (isLoading) { + showLoading(null) + } else { + dismissLoadingDialog() + } + } + private fun updateToolbar(devices: List, isSelectModeEnabled: Boolean) { invalidateOptionsMenu() val title = if (isSelectModeEnabled) { @@ -312,4 +326,37 @@ class OtherSessionsFragment : override fun onViewAllOtherSessionsClicked() { // NOOP. We don't have this button in this screen } + + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(OtherSessionsAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(OtherSessionsAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: OtherSessionsViewEvents.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt index 95f9c72b33..55753e35be 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt @@ -17,8 +17,14 @@ package im.vector.app.features.settings.devices.v2.othersessions import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse sealed class OtherSessionsViewEvents : VectorViewEvents { - data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents() - data class Failure(val throwable: Throwable) : OtherSessionsViewEvents() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : OtherSessionsViewEvents() + + object SignoutSuccess : OtherSessionsViewEvents() + data class SignoutError(val error: Throwable) : OtherSessionsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index cac5ce7d3b..052ec7025d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -21,19 +21,38 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase @@ -68,6 +87,9 @@ class OtherSessionsViewModel @AssistedInject constructor( // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { + is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action) + OtherSessionsAction.ReAuthCancelled -> handleReAuthCancelled() + OtherSessionsAction.SsoAuthDone -> handleSsoAuthDone() is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) OtherSessionsAction.DisableSelectMode -> handleDisableSelectMode() is OtherSessionsAction.EnableSelectMode -> handleEnableSelectMode(action.deviceId) @@ -145,7 +167,80 @@ class OtherSessionsViewModel @AssistedInject constructor( } } - private fun handleMultiSignout() { - // TODO call multi signout use case with all or only selected devices depending on the ViewState + private fun handleMultiSignout() = withState { state -> + viewModelScope.launch { + setLoading(true) + val deviceIds = getDeviceIdsToSignout(state) + if (deviceIds.isEmpty()) { + return@launch + } + val signoutResult = signout(deviceIds) + setLoading(false) + + if (signoutResult.isSuccess) { + onSignoutSuccess() + } else { + when (val failure = signoutResult.exceptionOrNull()) { + null -> onSignoutSuccess() + else -> onSignoutFailure(failure) + } + } + } + } + + private fun getDeviceIdsToSignout(state: OtherSessionsViewState): List { + return if (state.isSelectModeEnabled) { + state.devices()?.filter { it.isSelected }.orEmpty() + } else { + state.devices().orEmpty() + }.mapNotNull { it.deviceInfo.deviceId } + } + + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { + is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) + is SignoutSessionResult.Completed -> Unit + } + } + }) + + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + Timber.d("onReAuthNeeded") + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation + _viewEvents.post(OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } + + private fun setLoading(isLoading: Boolean) { + setState { copy(isLoading = isLoading) } + } + + private fun onSignoutSuccess() { + Timber.d("signout success") + refreshDeviceList() + _viewEvents.post(OtherSessionsViewEvents.SignoutSuccess) + } + + private fun onSignoutFailure(failure: Throwable) { + Timber.e("signout failure", failure) + val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { + stringProvider.getString(R.string.authentication_error) + } else { + stringProvider.getString(R.string.matrix_error) + } + _viewEvents.post(OtherSessionsViewEvents.SignoutError(Exception(failureMessage))) + } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: OtherSessionsAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt index 0db3c8cd0e..c0b50fded8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt @@ -27,6 +27,7 @@ data class OtherSessionsViewState( val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, val excludeCurrentDevice: Boolean = false, val isSelectModeEnabled: Boolean = false, + val isLoading: Boolean = false, ) : MavericksState { constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt index 60ca8e91c6..bc6cff0d43 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt @@ -21,6 +21,9 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.util.awaitCallback import javax.inject.Inject +/** + * Use case to signout a single session. + */ class SignoutSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt new file mode 100644 index 0000000000..82b03247c4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.util.awaitCallback +import javax.inject.Inject + +/** + * Use case to signout several sessions. + */ +class SignoutSessionsUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + // TODO add unit tests + suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { + return deleteDevices(deviceIds, userInteractiveAuthInterceptor) + } + + private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + } + } +} From 0f8e5919daeed2d2642215a99361f8dd63b0f07c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 17:52:13 +0200 Subject: [PATCH 086/679] Calling signout multi sessions use case in main screen for other sessions --- .../settings/devices/v2/DevicesAction.kt | 6 ++ .../settings/devices/v2/DevicesViewEvent.kt | 12 ++- .../settings/devices/v2/DevicesViewModel.kt | 97 ++++++++++++++++++- .../v2/VectorSettingsDevicesFragment.kt | 45 ++++++++- 4 files changed, 150 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index 9ecb72a25c..21cbb86e94 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { + // ReAuth + object SsoAuthDone : DevicesAction() + data class PasswordAuthDone(val password: String) : DevicesAction() + object ReAuthCancelled : DevicesAction() + + // Others object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() object MultiSignoutOtherSessions : DevicesAction() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt index c78c20f792..770ffc2513 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt @@ -17,17 +17,21 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo sealed class DevicesViewEvent : VectorViewEvents { - data class Loading(val message: CharSequence? = null) : DevicesViewEvent() - data class Failure(val throwable: Throwable) : DevicesViewEvent() - data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent() - data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : DevicesViewEvent() + data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent() object SelfVerification : DevicesViewEvent() data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent() object PromptResetSecrets : DevicesViewEvent() + object SignoutSuccess : DevicesViewEvent() + data class SignoutError(val error: Throwable) : DevicesViewEvent() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 8f12bf28b6..abe0e2719f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -21,24 +21,42 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @@ -98,6 +116,9 @@ class DevicesViewModel @AssistedInject constructor( // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { + is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action) + DevicesAction.ReAuthCancelled -> handleReAuthCancelled() + DevicesAction.SsoAuthDone -> handleSsoAuthDone() is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() @@ -119,7 +140,79 @@ class DevicesViewModel @AssistedInject constructor( // TODO implement when needed } - private fun handleMultiSignoutOtherSessions() { - // TODO call multi signout use case with all other devices than the current one + private fun handleMultiSignoutOtherSessions() = withState { state -> + viewModelScope.launch { + setLoading(true) + val deviceIds = getDeviceIdsOfOtherSessions(state) + if (deviceIds.isEmpty()) { + return@launch + } + val signoutResult = signout(deviceIds) + setLoading(false) + + if (signoutResult.isSuccess) { + onSignoutSuccess() + } else { + when (val failure = signoutResult.exceptionOrNull()) { + null -> onSignoutSuccess() + else -> onSignoutFailure(failure) + } + } + } + } + + private fun getDeviceIdsOfOtherSessions(state: DevicesViewState): List { + val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId + return state.devices() + ?.mapNotNull { fullInfo -> fullInfo.deviceInfo.deviceId.takeUnless { it == currentDeviceId } } + .orEmpty() + } + + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { + is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) + is SignoutSessionResult.Completed -> Unit + } + } + }) + + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + Timber.d("onReAuthNeeded") + pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation + _viewEvents.post(DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } + + private fun setLoading(isLoading: Boolean) { + setState { copy(isLoading = isLoading) } + } + + private fun onSignoutSuccess() { + Timber.d("signout success") + refreshDeviceList() + _viewEvents.post(DevicesViewEvent.SignoutSuccess) + } + + private fun onSignoutFailure(failure: Throwable) { + Timber.e("signout failure", failure) + val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { + stringProvider.getString(R.string.authentication_error) + } else { + stringProvider.getString(R.string.matrix_error) + } + _viewEvents.post(DevicesViewEvent.SignoutError(Exception(failureMessage))) + } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: DevicesAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index e9778e1368..4f507b2a3d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2 +import android.app.Activity import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -30,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.dialogs.ManuallyVerifyDialog +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -37,6 +39,7 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.features.VectorFeatures +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.login.qr.QrCodeLoginArgs @@ -48,6 +51,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -102,10 +106,7 @@ class VectorSettingsDevicesFragment : private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is DevicesViewEvent.Loading -> showLoading(it.message) - is DevicesViewEvent.Failure -> showFailure(it.throwable) - is DevicesViewEvent.RequestReAuth -> Unit // TODO. Next PR - is DevicesViewEvent.PromptRenameDevice -> Unit // TODO. Next PR + is DevicesViewEvent.RequestReAuth -> askForReAuthentication(it) is DevicesViewEvent.ShowVerifyDevice -> { VerificationBottomSheet.withArgs( roomId = null, @@ -124,6 +125,8 @@ class VectorSettingsDevicesFragment : is DevicesViewEvent.PromptResetSecrets -> { navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) } + is DevicesViewEvent.SignoutError -> showFailure(it.error) + is DevicesViewEvent.SignoutSuccess -> Unit // do nothing } } } @@ -137,6 +140,7 @@ class VectorSettingsDevicesFragment : views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { + // TODO ask for confirmation viewModel.handle(DevicesAction.MultiSignoutOtherSessions) true } @@ -366,4 +370,37 @@ class VectorSettingsDevicesFragment : excludeCurrentDevice = true ) } + + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(DevicesAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DevicesAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: DevicesViewEvent.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } From 727c7462df2d910ecfdb911b749345365edf8658 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 10:17:23 +0200 Subject: [PATCH 087/679] Adding confirmation dialog before signout process --- .../v2/VectorSettingsDevicesFragment.kt | 17 ++++++++- .../v2/othersessions/OtherSessionsFragment.kt | 16 +++++++- .../v2/overview/SessionOverviewFragment.kt | 12 ++---- .../BuildConfirmSignoutDialogUseCase.kt | 37 +++++++++++++++++++ 4 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 4f507b2a3d..98c7016d29 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -51,6 +51,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -75,6 +76,8 @@ class VectorSettingsDevicesFragment : @Inject lateinit var stringProvider: StringProvider + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + private val viewModel: DevicesViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding { @@ -140,8 +143,7 @@ class VectorSettingsDevicesFragment : views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { - // TODO ask for confirmation - viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + confirmMultiSignoutOtherSessions() true } else -> false @@ -149,6 +151,17 @@ class VectorSettingsDevicesFragment : } } + private fun confirmMultiSignoutOtherSessions() { + activity?.let { + buildConfirmSignoutDialogUseCase.execute(it, this::multiSignoutOtherSessions) + .show() + } + } + + private fun multiSignoutOtherSessions() { + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + } + private fun initOtherSessionsView() { views.deviceListOtherSessions.callback = this } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index ca9334ad08..d2bb1d443b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -49,6 +49,7 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -70,6 +71,8 @@ class OtherSessionsFragment : @Inject lateinit var viewNavigator: OtherSessionsViewNavigator + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) } @@ -124,13 +127,24 @@ class OtherSessionsFragment : true } R.id.otherSessionsMultiSignout -> { - viewModel.handle(OtherSessionsAction.MultiSignout) + confirmMultiSignout() true } else -> false } } + private fun confirmMultiSignout() { + activity?.let { + buildConfirmSignoutDialogUseCase.execute(it, this::multiSignout) + .show() + } + } + + private fun multiSignout() { + viewModel.handle(OtherSessionsAction.MultiSignout) + } + private fun enableSelectMode(isEnabled: Boolean, deviceId: String? = null) { val action = if (isEnabled) OtherSessionsAction.EnableSelectMode(deviceId) else OtherSessionsAction.DisableSelectMode viewModel.handle(action) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 620372f810..e149023f22 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -29,7 +29,6 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter @@ -45,6 +44,7 @@ import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -69,6 +69,8 @@ class SessionOverviewFragment : @Inject lateinit var stringProvider: StringProvider + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + private val viewModel: SessionOverviewViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding { @@ -134,13 +136,7 @@ class SessionOverviewFragment : private fun confirmSignoutOtherSession() { activity?.let { - MaterialAlertDialogBuilder(it) - .setTitle(R.string.action_sign_out) - .setMessage(R.string.action_sign_out_confirmation_simple) - .setPositiveButton(R.string.action_sign_out) { _, _ -> - signoutSession() - } - .setNegativeButton(R.string.action_cancel, null) + buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession) .show() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt new file mode 100644 index 0000000000..9959bd1828 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import javax.inject.Inject + +class BuildConfirmSignoutDialogUseCase @Inject constructor() { + + fun execute(context: Context, onConfirm: () -> Unit): AlertDialog { + return MaterialAlertDialogBuilder(context) + .setTitle(R.string.action_sign_out) + .setMessage(R.string.action_sign_out_confirmation_simple) + .setPositiveButton(R.string.action_sign_out) { _, _ -> + onConfirm() + } + .setNegativeButton(R.string.action_cancel, null) + .create() + } +} From a968ac08c363779d7cba8f3c91060e03ca21b53c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 14:20:38 +0200 Subject: [PATCH 088/679] Adding unit tests for signout sessions use case --- .../v2/signout/SignoutSessionsUseCase.kt | 1 - .../devices/v2/DevicesViewModelTest.kt | 28 +++++-- .../OtherSessionsViewModelTest.kt | 27 ++++-- .../v2/signout/SignoutSessionsUseCaseTest.kt | 83 +++++++++++++++++++ .../app/test/fakes/FakeCryptoService.kt | 14 ++++ 5 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt index 82b03247c4..b4fc78043e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -28,7 +28,6 @@ class SignoutSessionsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { - // TODO add unit tests suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { return deleteDevices(deviceIds, userInteractiveAuthInterceptor) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index c5edfb868d..bf06dd7329 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -22,10 +22,14 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test import im.vector.app.test.testDispatcher @@ -53,21 +57,29 @@ class DevicesViewModelTest { val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() - private val refreshDevicesUseCase = mockk(relaxUnitFun = true) private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() + private val fakeSignoutSessionsUseCase = mockk() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() + private val fakePendingAuthHandler = FakePendingAuthHandler() + private val refreshDevicesUseCase = mockk(relaxUnitFun = true) private fun createViewModel(): DevicesViewModel { return DevicesViewModel( - DevicesViewState(), - fakeActiveSessionHolder.instance, - getCurrentSessionCrossSigningInfoUseCase, - getDeviceFullInfoListUseCase, - refreshDevicesOnCryptoDevicesChangeUseCase, - checkIfCurrentSessionCanBeVerifiedUseCase, - refreshDevicesUseCase, + initialState = DevicesViewState(), + activeSessionHolder = fakeActiveSessionHolder.instance, + stringProvider = fakeStringProvider.instance, + getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, + getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, + refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, + checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, + pendingAuthHandler = fakePendingAuthHandler.instance, + refreshDevicesUseCase = refreshDevicesUseCase, ) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index e7b8eeee9b..7cf624e569 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -23,7 +23,11 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -54,15 +58,24 @@ class OtherSessionsViewModelTest { ) private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCaseUseCase = mockk() - - private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = OtherSessionsViewModel( - initialState = OtherSessionsViewState(args), - activeSessionHolder = fakeActiveSessionHolder.instance, - getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, - refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, - ) + private val fakeSignoutSessionsUseCase = mockk() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() + private val fakePendingAuthHandler = FakePendingAuthHandler() + + private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = + OtherSessionsViewModel( + initialState = OtherSessionsViewState(args), + stringProvider = fakeStringProvider.instance, + activeSessionHolder = fakeActiveSessionHolder.instance, + getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, + pendingAuthHandler = fakePendingAuthHandler.instance, + refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, + ) @Before fun setup() { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt new file mode 100644 index 0000000000..208ce8b334 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.signout + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.Test +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor + +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" + +class SignoutSessionsUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val signoutSessionsUseCase = SignoutSessionsUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance + ) + + @Test + fun `given a list of device ids when signing out with success then success result is returned`() = runTest { + // Given + val interceptor = givenAuthInterceptor() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesSucceeds(deviceIds) + + // When + val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + + // Then + result.isSuccess shouldBe true + every { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, interceptor, any()) + } + } + + @Test + fun `given a list of device ids when signing out with error then failure result is returned`() = runTest { + // Given + val interceptor = givenAuthInterceptor() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + val error = mockk() + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesFailsWithError(deviceIds, error) + + // When + val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + + // Then + result.isFailure shouldBe true + every { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, interceptor, any()) + } + } + + private fun givenAuthInterceptor() = mockk() +} + diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index e96a58faa0..5f34c45fa7 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -84,5 +84,19 @@ class FakeCryptoService( } } + fun givenDeleteDevicesSucceeds(deviceIds: List) { + val matrixCallback = slot>() + every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + thirdArg>().onSuccess(Unit) + } + } + + fun givenDeleteDevicesFailsWithError(deviceIds: List, error: Exception) { + val matrixCallback = slot>() + every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + thirdArg>().onFailure(error) + } + } + override fun getMyDevice() = cryptoDeviceInfo } From 5bcf2ac51ea4ce40b43700e0f03d662985872b5f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 15:37:13 +0200 Subject: [PATCH 089/679] Adding unit tests for other sessions list view model --- .../othersessions/OtherSessionsViewModel.kt | 1 - .../OtherSessionsViewModelTest.kt | 295 +++++++++++++++++- .../overview/SessionOverviewViewModelTest.kt | 109 +++---- .../test/fakes/FakeSignoutSessionUseCase.kt | 77 +++++ .../test/fakes/FakeSignoutSessionsUseCase.kt | 77 +++++ 5 files changed, 479 insertions(+), 80 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index 052ec7025d..a26187b797 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -84,7 +84,6 @@ class OtherSessionsViewModel @AssistedInject constructor( } } - // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 7cf624e569..28b97ed70d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -19,32 +19,43 @@ package im.vector.app.features.settings.devices.v2.othersessions import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll +import io.mockk.verify import io.mockk.verifyAll import kotlinx.coroutines.flow.flowOf +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.net.ssl.HttpsURLConnection private const val A_TITLE_RES_ID = 1 -private const val A_DEVICE_ID = "device-id" +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" +private const val A_PASSWORD = "password" +private const val AUTH_ERROR_MESSAGE = "auth-error-message" +private const val AN_ERROR_MESSAGE = "error-message" class OtherSessionsViewModelTest { @@ -60,18 +71,18 @@ class OtherSessionsViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() - private val fakeRefreshDevicesUseCaseUseCase = mockk() - private val fakeSignoutSessionsUseCase = mockk() + private val fakeRefreshDevicesUseCaseUseCase = mockk(relaxed = true) + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = + private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( - initialState = OtherSessionsViewState(args), + initialState = viewState, stringProvider = fakeStringProvider.instance, activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, - signoutSessionsUseCase = fakeSignoutSessionsUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, @@ -101,6 +112,39 @@ class OtherSessionsViewModelTest { unmockkAll() } + @Test + fun `given the viewModel when initializing it then verification listener is added`() { + // Given + val fakeVerificationService = givenVerificationService() + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + + // When + val viewModel = createViewModel() + + // Then + verify { + fakeVerificationService.addListener(viewModel) + } + } + + @Test + fun `given the viewModel when clearing it then verification listener is removed`() { + // Given + val fakeVerificationService = givenVerificationService() + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + + // When + val viewModel = createViewModel() + viewModel.onCleared() + + // Then + verify { + fakeVerificationService.removeListener(viewModel) + } + } + @Test fun `given the viewModel has been initialized then viewState is updated with devices list`() { // Given @@ -156,7 +200,7 @@ class OtherSessionsViewModelTest { @Test fun `given enable select mode action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) + val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) val devices: List = listOf(deviceFullInfo) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -169,7 +213,7 @@ class OtherSessionsViewModelTest { // When val viewModel = createViewModel() val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID)) + viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID_1)) // Then viewModelTest @@ -180,8 +224,8 @@ class OtherSessionsViewModelTest { @Test fun `given disable select mode action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -205,7 +249,7 @@ class OtherSessionsViewModelTest { @Test fun `given toggle selection for device action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) + val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) val devices: List = listOf(deviceFullInfo) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -218,7 +262,7 @@ class OtherSessionsViewModelTest { // When val viewModel = createViewModel() val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID)) + viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID_1)) // Then viewModelTest @@ -229,8 +273,8 @@ class OtherSessionsViewModelTest { @Test fun `given select all action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -254,8 +298,8 @@ class OtherSessionsViewModelTest { @Test fun `given deselect all action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -276,6 +320,223 @@ class OtherSessionsViewModelTest { .finish() } + @Test + fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() { + // Given + val isSelectModeEnabled = true + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + // signout only selected devices + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + isSelectModeEnabled = isSelectModeEnabled, + ) + + // When + val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled)) + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCaseUseCase.execute() + } + } + + @Test + fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() { + // Given + val isSelectModeEnabled = false + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + // signout all devices + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + isSelectModeEnabled = isSelectModeEnabled, + ) + + // When + val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled)) + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCaseUseCase.execute() + } + } + + @Test + fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + ) + fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val error = Exception() + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + ) + fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertEvent { it == expectedReAuthEvent } + .finish() + fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth + fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation + } + + @Test + fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() { + // Given + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.ssoAuthDone() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.SsoAuthDone) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.ssoAuthDone() + } + } + + @Test + fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() { + // Given + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.PasswordAuthDone(A_PASSWORD)) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD) + } + } + + @Test + fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() { + // Given + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.reAuthCancelled() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.reAuthCancelled() + } + } + private fun givenGetDeviceFullInfoListReturns( filterType: DeviceManagerFilterType, devices: List, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index c0ba6ce28b..289279b8f6 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -26,11 +26,10 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService @@ -43,7 +42,6 @@ import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs -import io.mockk.slot import io.mockk.unmockkAll import io.mockk.verify import io.mockk.verifyAll @@ -53,14 +51,10 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import javax.net.ssl.HttpsURLConnection -import kotlin.coroutines.Continuation private const val A_SESSION_ID_1 = "session-id-1" private const val A_SESSION_ID_2 = "session-id-2" @@ -83,10 +77,10 @@ class SessionOverviewViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val signoutSessionUseCase = mockk() + private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private val refreshDevicesUseCase = mockk() + private val refreshDevicesUseCase = mockk(relaxed = true) private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() private val fakeGetNotificationsStatusUseCase = mockk() private val notificationsStatus = NotificationsStatus.ENABLED @@ -96,7 +90,7 @@ class SessionOverviewViewModelTest { stringProvider = fakeStringProvider.instance, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionUseCase = signoutSessionUseCase, + signoutSessionUseCase = fakeSignoutSessionUseCase.instance, interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, @@ -115,11 +109,50 @@ class SessionOverviewViewModelTest { every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) } + private fun givenVerificationService(): FakeVerificationService { + val fakeVerificationService = fakeActiveSessionHolder + .fakeSession + .fakeCryptoService + .fakeVerificationService + fakeVerificationService.givenAddListenerSucceeds() + fakeVerificationService.givenRemoveListenerSucceeds() + return fakeVerificationService + } + @After fun tearDown() { unmockkAll() } + @Test + fun `given the viewModel when initializing it then verification listener is added`() { + // Given + val fakeVerificationService = givenVerificationService() + + // When + val viewModel = createViewModel() + + // Then + verify { + fakeVerificationService.addListener(viewModel) + } + } + + @Test + fun `given the viewModel when clearing it then verification listener is removed`() { + // Given + val fakeVerificationService = givenVerificationService() + + // When + val viewModel = createViewModel() + viewModel.onCleared() + + // Then + verify { + fakeVerificationService.removeListener(viewModel) + } + } + @Test fun `given the viewModel has been initialized then pushers are refreshed`() { createViewModel() @@ -223,8 +256,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - givenSignoutSuccess(A_SESSION_ID_1) - every { refreshDevicesUseCase.execute() } just runs + fakeSignoutSessionUseCase.givenSignoutSuccess(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -261,7 +293,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - givenSignoutError(A_SESSION_ID_1, serverError) + fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, serverError) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -296,7 +328,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val error = Exception() - givenSignoutError(A_SESSION_ID_1, error) + fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, error) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -330,7 +362,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val reAuthNeeded = givenSignoutReAuthNeeded(A_SESSION_ID_1) + val reAuthNeeded = fakeSignoutSessionUseCase.givenSignoutReAuthNeeded(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) @@ -415,53 +447,6 @@ class SessionOverviewViewModelTest { } } - private fun givenSignoutSuccess(deviceId: String) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - } - - private fun givenSignoutReAuthNeeded(deviceId: String): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() - val flowResponse = mockk() - every { flowResponse.session } returns A_SESSION_ID_1 - val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( - pendingAuth = mockk(), - uiaContinuation = promise, - flowResponse = flowResponse, - errCode = errorCode, - ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - - return reAuthNeeded - } - - private fun givenSignoutError(deviceId: String, error: Throwable) { - coEvery { signoutSessionUseCase.execute(deviceId, any()) } returns Result.failure(error) - } - - private fun givenVerificationService(): FakeVerificationService { - val fakeVerificationService = fakeActiveSessionHolder - .fakeSession - .fakeCryptoService - .fakeVerificationService - fakeVerificationService.givenAddListenerSucceeds() - fakeVerificationService.givenRemoveListenerSucceeds() - return fakeVerificationService - } - private fun givenCurrentSessionIsTrusted() { fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2) val deviceFullInfo = mockk() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt new file mode 100644 index 0000000000..8a6b101ff6 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import kotlin.coroutines.Continuation + +class FakeSignoutSessionUseCase { + + val instance = mockk() + + fun givenSignoutSuccess( + deviceId: String, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ) { + val interceptor = slot() + val flowResponse = mockk() + val errorCode = "errorCode" + val promise = mockk>() + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed + coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + } + + fun givenSignoutReAuthNeeded( + deviceId: String, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ): SignoutSessionResult.ReAuthNeeded { + val interceptor = slot() + val flowResponse = mockk() + every { flowResponse.session } returns "a-session-id" + val errorCode = "errorCode" + val promise = mockk>() + val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errorCode, + ) + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded + coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + + return reAuthNeeded + } + + fun givenSignoutError(deviceId: String, error: Throwable) { + coEvery { instance.execute(deviceId, any()) } returns Result.failure(error) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt new file mode 100644 index 0000000000..04d05b1d8a --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import kotlin.coroutines.Continuation + +class FakeSignoutSessionsUseCase { + + val instance = mockk() + + fun givenSignoutSuccess( + deviceIds: List, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ) { + val interceptor = slot() + val flowResponse = mockk() + val errorCode = "errorCode" + val promise = mockk>() + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed + coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + } + + fun givenSignoutReAuthNeeded( + deviceIds: List, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ): SignoutSessionResult.ReAuthNeeded { + val interceptor = slot() + val flowResponse = mockk() + every { flowResponse.session } returns "a-session-id" + val errorCode = "errorCode" + val promise = mockk>() + val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errorCode, + ) + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded + coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + + return reAuthNeeded + } + + fun givenSignoutError(deviceIds: List, error: Throwable) { + coEvery { instance.execute(deviceIds, any()) } returns Result.failure(error) + } +} From 880ee4058c2fe50f6fa10edff53c6a892cb88ef9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 16:14:32 +0200 Subject: [PATCH 090/679] Adding unit tests about reAuth actions for devices view model --- .../settings/devices/v2/DevicesViewModel.kt | 1 - .../devices/v2/DevicesViewModelTest.kt | 214 ++++++++++++++---- 2 files changed, 170 insertions(+), 45 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index abe0e2719f..fe4d0dc838 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -113,7 +113,6 @@ class DevicesViewModel @AssistedInject constructor( } } - // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index bf06dd7329..71e9d609b7 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -19,16 +19,17 @@ package im.vector.app.features.settings.devices.v2 import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.R import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -36,20 +37,32 @@ import im.vector.app.test.testDispatcher import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every -import io.mockk.just +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.runs import io.mockk.unmockkAll import io.mockk.verify +import io.mockk.verifyAll import kotlinx.coroutines.flow.flowOf +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.net.ssl.HttpsURLConnection + +private const val A_CURRENT_DEVICE_ID = "current-device-id" +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" +private const val A_PASSWORD = "password" +private const val AUTH_ERROR_MESSAGE = "auth-error-message" +private const val AN_ERROR_MESSAGE = "error-message" class DevicesViewModelTest { @@ -60,9 +73,9 @@ class DevicesViewModelTest { private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() - private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk() + private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val fakeSignoutSessionsUseCase = mockk() + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxUnitFun = true) @@ -76,7 +89,7 @@ class DevicesViewModelTest { getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionsUseCase = fakeSignoutSessionsUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = refreshDevicesUseCase, @@ -88,6 +101,20 @@ class DevicesViewModelTest { // Needed for internal usage of Flow.throttleFirst() inside the ViewModel mockkStatic(SystemClock::class) every { SystemClock.elapsedRealtime() } returns 1234 + + givenVerificationService() + givenCurrentSessionCrossSigningInfo() + givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + } + + private fun givenVerificationService(): FakeVerificationService { + val fakeVerificationService = fakeActiveSessionHolder + .fakeSession + .fakeCryptoService + .fakeVerificationService + fakeVerificationService.givenAddListenerSucceeds() + fakeVerificationService.givenRemoveListenerSucceeds() + return fakeVerificationService } @After @@ -99,9 +126,6 @@ class DevicesViewModelTest { fun `given the viewModel when initializing it then verification listener is added`() { // Given val fakeVerificationService = givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModel = createViewModel() @@ -116,9 +140,6 @@ class DevicesViewModelTest { fun `given the viewModel when clearing it then verification listener is removed`() { // Given val fakeVerificationService = givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModel = createViewModel() @@ -133,10 +154,7 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then view state is updated with current session cross signing info`() { // Given - givenVerificationService() val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModelTest = createViewModel().test() @@ -149,10 +167,7 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then view state is updated with current device full info list`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - val deviceFullInfoList = givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) // When val viewModelTest = createViewModel().test() @@ -168,10 +183,6 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When createViewModel() @@ -183,10 +194,6 @@ class DevicesViewModelTest { @Test fun `given current session can be verified when handling verify current session action then self verification event is posted`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true @@ -207,10 +214,6 @@ class DevicesViewModelTest { @Test fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false @@ -228,18 +231,128 @@ class DevicesViewModelTest { } } - private fun givenVerificationService(): FakeVerificationService { - val fakeVerificationService = fakeActiveSessionHolder - .fakeSession - .fakeCryptoService - .fakeVerificationService - fakeVerificationService.givenAddListenerSucceeds() - fakeVerificationService.givenRemoveListenerSucceeds() - return fakeVerificationService + @Test + fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { + // Given + val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) + val expectedViewState = givenInitialViewState() + fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val error = Exception() + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) + val expectedViewState = givenInitialViewState() + fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { + // Given + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertEvent { it == expectedReAuthEvent } + .finish() + fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth + fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation + } + + @Test + fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() { + // Given + justRun { fakePendingAuthHandler.instance.ssoAuthDone() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.SsoAuthDone) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.ssoAuthDone() + } + } + + @Test + fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() { + // Given + justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.PasswordAuthDone(A_PASSWORD)) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD) + } + } + + @Test + fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() { + // Given + justRun { fakePendingAuthHandler.instance.reAuthCancelled() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.ReAuthCancelled) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.reAuthCancelled() + } } private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo { val currentSessionCrossSigningInfo = mockk() + every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo) return currentSessionCrossSigningInfo } @@ -247,14 +360,19 @@ class DevicesViewModelTest { /** * Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active. */ - private fun givenDeviceFullInfoList(): List { + private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): List { val verifiedCryptoDeviceInfo = mockk() every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) val unverifiedCryptoDeviceInfo = mockk() every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) + val deviceInfo1 = mockk() + every { deviceInfo1.deviceId } returns deviceId1 + val deviceInfo2 = mockk() + every { deviceInfo2.deviceId } returns deviceId2 + val deviceFullInfo1 = DeviceFullInfo( - deviceInfo = mockk(), + deviceInfo = deviceInfo1, cryptoDeviceInfo = verifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = false, @@ -263,7 +381,7 @@ class DevicesViewModelTest { matrixClientInfo = MatrixClientInfoContent(), ) val deviceFullInfo2 = DeviceFullInfo( - deviceInfo = mockk(), + deviceInfo = deviceInfo2, cryptoDeviceInfo = unverifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, isInactive = true, @@ -277,7 +395,15 @@ class DevicesViewModelTest { return deviceFullInfoList } - private fun givenRefreshDevicesOnCryptoDevicesChange() { - coEvery { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } just runs + private fun givenInitialViewState(): DevicesViewState { + val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + return DevicesViewState( + currentSessionCrossSigningInfo = currentSessionCrossSigningInfo, + devices = Success(deviceFullInfoList), + unverifiedSessionsCount = 1, + inactiveSessionsCount = 1, + isLoading = false, + ) } } From a3df90ae3ec70e3e5427b16382855099d3db95b2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:03:13 +0200 Subject: [PATCH 091/679] Adding unit tests about multi signout action for devices view model --- .../devices/v2/DevicesViewModelTest.kt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 71e9d609b7..7ece9cf877 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -78,7 +78,7 @@ class DevicesViewModelTest { private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private val refreshDevicesUseCase = mockk(relaxUnitFun = true) + private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -92,7 +92,7 @@ class DevicesViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, - refreshDevicesUseCase = refreshDevicesUseCase, + refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) } @@ -231,12 +231,38 @@ class DevicesViewModelTest { } } + @Test + fun `given no reAuth is needed when handling multiSignout other sessions action then signout process is performed`() { + // Given + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID) + // signout all devices except the current device + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1), fakeInterceptSignoutFlowResponseUseCase) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCase.execute() + } + } + @Test fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { // Given val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = givenInitialViewState() + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) // When @@ -260,7 +286,7 @@ class DevicesViewModelTest { // Given val error = Exception() fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) - val expectedViewState = givenInitialViewState() + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When @@ -395,9 +421,9 @@ class DevicesViewModelTest { return deviceFullInfoList } - private fun givenInitialViewState(): DevicesViewState { + private fun givenInitialViewState(deviceId1: String, deviceId2: String): DevicesViewState { val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() - val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1, deviceId2) return DevicesViewState( currentSessionCrossSigningInfo = currentSessionCrossSigningInfo, devices = Success(deviceFullInfoList), From e0d511a4880fb0a4c4aa305b02b60782dea2af99 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:05:01 +0200 Subject: [PATCH 092/679] Fixing a name of a mocked component --- .../v2/othersessions/OtherSessionsViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 28b97ed70d..f282e5ca82 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -71,7 +71,7 @@ class OtherSessionsViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() - private val fakeRefreshDevicesUseCaseUseCase = mockk(relaxed = true) + private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() @@ -85,7 +85,7 @@ class OtherSessionsViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, - refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, + refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) @Before @@ -352,7 +352,7 @@ class OtherSessionsViewModelTest { .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } .finish() verify { - fakeRefreshDevicesUseCaseUseCase.execute() + fakeRefreshDevicesUseCase.execute() } } @@ -388,7 +388,7 @@ class OtherSessionsViewModelTest { .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } .finish() verify { - fakeRefreshDevicesUseCaseUseCase.execute() + fakeRefreshDevicesUseCase.execute() } } From 4b0b335a687e8ea9abe0ce5b2ae219020c03a39d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:06:32 +0200 Subject: [PATCH 093/679] Fixing code quality issues --- .../vector/app/features/settings/devices/v2/DevicesViewEvent.kt | 2 -- .../settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt index 770ffc2513..9f5257693e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt @@ -17,10 +17,8 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents -import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo sealed class DevicesViewEvent : VectorViewEvents { data class RequestReAuth( diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt index 208ce8b334..08a9fa625b 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -80,4 +80,3 @@ class SignoutSessionsUseCaseTest { private fun givenAuthInterceptor() = mockk() } - From 76e2b6b39f5300944107e62ac3eb5cf27fe54005 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:09:51 +0200 Subject: [PATCH 094/679] Removing some TODOs --- .../matrix/android/sdk/internal/crypto/DefaultCryptoService.kt | 2 -- .../android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 032d649421..7862da1c17 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -241,12 +241,10 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } - // TODO add unit test override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback) } - // TODO add unit test override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index fc6bc9b1bc..12b3fbd624 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -37,7 +37,6 @@ internal interface DeleteDeviceTask : Task { ) } -// TODO add unit tests internal class DefaultDeleteDeviceTask @Inject constructor( private val cryptoApi: CryptoApi, private val globalErrorReceiver: GlobalErrorReceiver From db42d1c01cd2b98c0027cc8c315a4d7f1bf0bad3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 12:31:57 +0200 Subject: [PATCH 095/679] Fix post rebase unit tests --- .../OtherSessionsViewModelTest.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index f282e5ca82..f899e3c657 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -324,8 +324,8 @@ class OtherSessionsViewModelTest { fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() { // Given val isSelectModeEnabled = true - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout only selected devices @@ -360,8 +360,8 @@ class OtherSessionsViewModelTest { fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() { // Given val isSelectModeEnabled = false - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout all devices @@ -395,8 +395,8 @@ class OtherSessionsViewModelTest { @Test fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) @@ -427,8 +427,8 @@ class OtherSessionsViewModelTest { @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val error = Exception() @@ -459,8 +459,8 @@ class OtherSessionsViewModelTest { @Test fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) From ef5aaf752554385428a0b9f0c88e52fd19a1a6dc Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:10:31 +0200 Subject: [PATCH 096/679] Fix forbidden usage of AlertDialog --- .../BuildConfirmSignoutDialogUseCase.kt | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt index 9959bd1828..4edfc2febe 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt @@ -17,21 +17,19 @@ package im.vector.app.features.settings.devices.v2.signout import android.content.Context -import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import javax.inject.Inject class BuildConfirmSignoutDialogUseCase @Inject constructor() { - fun execute(context: Context, onConfirm: () -> Unit): AlertDialog { - return MaterialAlertDialogBuilder(context) - .setTitle(R.string.action_sign_out) - .setMessage(R.string.action_sign_out_confirmation_simple) - .setPositiveButton(R.string.action_sign_out) { _, _ -> - onConfirm() - } - .setNegativeButton(R.string.action_cancel, null) - .create() - } + fun execute(context: Context, onConfirm: () -> Unit) = + MaterialAlertDialogBuilder(context) + .setTitle(R.string.action_sign_out) + .setMessage(R.string.action_sign_out_confirmation_simple) + .setPositiveButton(R.string.action_sign_out) { _, _ -> + onConfirm() + } + .setNegativeButton(R.string.action_cancel, null) + .create() } From d2d9da3ef73584e9367ad443dd20851b52e6ed1c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:17:37 +0200 Subject: [PATCH 097/679] Exclude the current session from other sessions and security recommendation screens --- .../settings/devices/v2/VectorSettingsDevicesFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 98c7016d29..3a3c3463fb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -173,7 +173,7 @@ class VectorSettingsDevicesFragment : requireActivity(), R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.UNVERIFIED, - excludeCurrentDevice = false + excludeCurrentDevice = true ) } } @@ -183,7 +183,7 @@ class VectorSettingsDevicesFragment : requireActivity(), R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.INACTIVE, - excludeCurrentDevice = false + excludeCurrentDevice = true ) } } From 3c7ba85c2604f886def970c5aa6f13656cabe7b1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 16:03:22 +0200 Subject: [PATCH 098/679] Removing unused string --- library/ui-strings/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index e772748a41..cd7cb3f477 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3345,7 +3345,6 @@ No inactive sessions found. Clear Filter Select sessions - Sign out of these sessions Sign out Sign out of %1$d session From 5515cd379f5f0a76f3efa8e0ccdaeeb864dd01f1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 16:04:51 +0200 Subject: [PATCH 099/679] Use SHOW_AS_ACTION_IF_ROOM tag --- .../settings/devices/v2/othersessions/OtherSessionsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index d2bb1d443b..487531646a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -102,7 +102,7 @@ class OtherSessionsFragment : } else { viewState.devices.invoke()?.isNotEmpty().orFalse() } - val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) changeTextColorOfDestructiveAction(multiSignoutItem) } From 1d2b8e76a289fcbc3996d16379136521566221e1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 11:13:23 +0100 Subject: [PATCH 100/679] Adding min size annotation to task params --- .../android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 12b3fbd624..549122447e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.tasks +import androidx.annotation.Size import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.uia.UiaResult @@ -31,7 +32,7 @@ import javax.inject.Inject internal interface DeleteDeviceTask : Task { data class Params( - val deviceIds: List, + @Size(min = 1) val deviceIds: List, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, val userAuthParam: UIABaseAuth? ) From 45050e821648b83e62a549abb182ce4ac981429f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 11:34:04 +0100 Subject: [PATCH 101/679] Removing error formatting from ViewModel --- .../settings/devices/v2/DevicesViewModel.kt | 12 +---- .../othersessions/OtherSessionsViewModel.kt | 12 +---- .../v2/overview/SessionOverviewViewModel.kt | 12 +---- .../devices/v2/DevicesViewModelTest.kt | 35 +------------- .../OtherSessionsViewModelTest.kt | 43 +---------------- .../overview/SessionOverviewViewModelTest.kt | 46 +------------------ 6 files changed, 6 insertions(+), 154 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index fe4d0dc838..c714645b9a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -21,11 +21,9 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase @@ -40,16 +38,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, activeSessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, @@ -195,12 +190,7 @@ class DevicesViewModel @AssistedInject constructor( private fun onSignoutFailure(failure: Throwable) { Timber.e("signout failure", failure) - val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { - stringProvider.getString(R.string.authentication_error) - } else { - stringProvider.getString(R.string.matrix_error) - } - _viewEvents.post(DevicesViewEvent.SignoutError(Exception(failureMessage))) + _viewEvents.post(DevicesViewEvent.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index a26187b797..c33490400b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -21,11 +21,9 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase @@ -39,16 +37,13 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, @@ -223,12 +218,7 @@ class OtherSessionsViewModel @AssistedInject constructor( private fun onSignoutFailure(failure: Throwable) { Timber.e("signout failure", failure) - val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { - stringProvider.getString(R.string.authentication_error) - } else { - stringProvider.getString(R.string.matrix_error) - } - _viewEvents.post(OtherSessionsViewEvents.SignoutError(Exception(failureMessage))) + _viewEvents.post(OtherSessionsViewEvents.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index e6aa7c2747..59eeaaadb4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -21,11 +21,9 @@ import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel @@ -44,16 +42,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, - private val stringProvider: StringProvider, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val signoutSessionUseCase: SignoutSessionUseCase, @@ -196,12 +191,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun onSignoutFailure(failure: Throwable) { Timber.e("signout failure", failure) - val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { - stringProvider.getString(R.string.authentication_error) - } else { - stringProvider.getString(R.string.matrix_error) - } - _viewEvents.post(SessionOverviewViewEvent.SignoutError(Exception(failureMessage))) + _viewEvents.post(SessionOverviewViewEvent.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 7ece9cf877..852fc64fd5 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -19,7 +19,6 @@ package im.vector.app.features.settings.devices.v2 import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType @@ -30,7 +29,6 @@ import im.vector.app.features.settings.devices.v2.verification.GetCurrentSession import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test import im.vector.app.test.testDispatcher @@ -49,20 +47,16 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_CURRENT_DEVICE_ID = "current-device-id" private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" private const val A_PASSWORD = "password" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" class DevicesViewModelTest { @@ -70,7 +64,6 @@ class DevicesViewModelTest { val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) @@ -84,7 +77,6 @@ class DevicesViewModelTest { return DevicesViewModel( initialState = DevicesViewState(), activeSessionHolder = fakeActiveSessionHolder.instance, - stringProvider = fakeStringProvider.instance, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, @@ -257,37 +249,12 @@ class DevicesViewModelTest { } } - @Test - fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { - // Given - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(DevicesAction.MultiSignoutOtherSessions) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given val error = Exception() fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -301,7 +268,7 @@ class DevicesViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is DevicesViewEvent.SignoutError && it.error == error } .finish() } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index f899e3c657..e01d6e058c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -19,7 +19,6 @@ package im.vector.app.features.settings.devices.v2.othersessions import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase @@ -28,7 +27,6 @@ import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowRe import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -46,16 +44,12 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_TITLE_RES_ID = 1 private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" private const val A_PASSWORD = "password" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" class OtherSessionsViewModelTest { @@ -69,7 +63,6 @@ class OtherSessionsViewModelTest { ) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() @@ -79,7 +72,6 @@ class OtherSessionsViewModelTest { private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( initialState = viewState, - stringProvider = fakeStringProvider.instance, activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, @@ -392,38 +384,6 @@ class OtherSessionsViewModelTest { } } - @Test - fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { - // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) - val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) - givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = OtherSessionsViewState( - devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), - currentFilter = defaultArgs.defaultFilter, - excludeCurrentDevice = defaultArgs.excludeCurrentDevice, - ) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.MultiSignout) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given @@ -438,7 +398,6 @@ class OtherSessionsViewModelTest { currentFilter = defaultArgs.defaultFilter, excludeCurrentDevice = defaultArgs.excludeCurrentDevice, ) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -452,7 +411,7 @@ class OtherSessionsViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error == error } .finish() } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 289279b8f6..b2ab939bd1 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -20,7 +20,6 @@ import android.os.SystemClock import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase @@ -30,7 +29,6 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -51,15 +49,11 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_SESSION_ID_1 = "session-id-1" private const val A_SESSION_ID_2 = "session-id-2" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" private const val A_PASSWORD = "password" class SessionOverviewViewModelTest { @@ -75,7 +69,6 @@ class SessionOverviewViewModelTest { ) private val getDeviceFullInfoUseCase = mockk(relaxed = true) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() private val interceptSignoutFlowResponseUseCase = mockk() @@ -87,7 +80,6 @@ class SessionOverviewViewModelTest { private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), - stringProvider = fakeStringProvider.instance, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, signoutSessionUseCase = fakeSignoutSessionUseCase.instance, @@ -286,41 +278,6 @@ class SessionOverviewViewModelTest { } } - @Test - fun `given another session and server error during signout when handling signout action then signout process is performed`() { - // Given - val deviceFullInfo = mockk() - every { deviceFullInfo.isCurrentDevice } returns false - every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, serverError) - val signoutAction = SessionOverviewAction.SignoutOtherSession - givenCurrentSessionIsTrusted() - val expectedViewState = SessionOverviewViewState( - deviceId = A_SESSION_ID_1, - isCurrentSessionTrusted = true, - deviceInfo = Success(deviceFullInfo), - isLoading = false, - notificationsStatus = notificationsStatus, - ) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(signoutAction) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given another session and unexpected error during signout when handling signout action then signout process is performed`() { // Given @@ -338,7 +295,6 @@ class SessionOverviewViewModelTest { isLoading = false, notificationsStatus = notificationsStatus, ) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -352,7 +308,7 @@ class SessionOverviewViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error == error } .finish() } From f0340d5cedf0be689885280a924b8718dd44b421 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 7 Nov 2022 11:58:34 +0100 Subject: [PATCH 102/679] When joining a room, the message composer should be visible once the room loads (#7510) --- changelog.d/7509.bugfix | 1 + .../vector/app/features/home/room/detail/TimelineFragment.kt | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/7509.bugfix diff --git a/changelog.d/7509.bugfix b/changelog.d/7509.bugfix new file mode 100644 index 0000000000..93ec812e0e --- /dev/null +++ b/changelog.d/7509.bugfix @@ -0,0 +1 @@ +When joining a room, the message composer is displayed once the room is loaded. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 120e5e22cb..60dd1320d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1169,6 +1169,9 @@ class TimelineFragment : lazyLoadedViews.inviteView(false)?.isVisible = false if (mainState.tombstoneEvent == null) { + views.composerContainer.isInvisible = !messageComposerState.isComposerVisible + views.voiceMessageRecorderContainer.isVisible = messageComposerState.isVoiceMessageRecorderVisible + when (messageComposerState.canSendMessage) { CanSendStatus.Allowed -> { NotificationAreaView.State.Hidden @@ -1224,6 +1227,7 @@ class TimelineFragment : private fun FragmentTimelineBinding.hideComposerViews() { composerContainer.isVisible = false + voiceMessageRecorderContainer.isVisible = false } private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) { From 44c0378de8b59a69a7d585bfb83cda8418273205 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 7 Nov 2022 14:46:32 +0300 Subject: [PATCH 103/679] Fix description of verified sessions. --- library/ui-strings/src/main/res/values/strings.xml | 2 ++ .../devices/v2/othersessions/OtherSessionsFragment.kt | 5 ++++- .../settings/devices/v2/overview/SessionOverviewFragment.kt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 450eb64849..370005363d 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3373,7 +3373,9 @@ Unverified sessions Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. Verified sessions + Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. + Verified sessions are anywhere you are using ${app_name} after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. Renaming sessions Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. Enable new session manager diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 4f1c8353f5..c5c7ab634c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -196,7 +196,10 @@ class OtherSessionsFragment : ) ) views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found) - updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_verified_title, R.string.device_manager_learn_more_sessions_verified) + updateSecurityLearnMoreButton( + R.string.device_manager_learn_more_sessions_verified_title, + R.string.device_manager_learn_more_sessions_verified_description + ) } DeviceManagerFilterType.UNVERIFIED -> { views.otherSessionsSecurityRecommendationView.render( diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 620372f810..c1d332fd23 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -284,7 +284,7 @@ class SessionOverviewFragment : R.string.device_manager_verification_status_unverified } val descriptionResId = if (isVerified) { - R.string.device_manager_learn_more_sessions_verified + R.string.device_manager_learn_more_sessions_verified_description } else { R.string.device_manager_learn_more_sessions_unverified } From e30cbd5916c25916da438dd2afed5aff68f1edb9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 7 Nov 2022 14:51:18 +0300 Subject: [PATCH 104/679] Add changelog. --- changelog.d/7533.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7533.bugfix diff --git a/changelog.d/7533.bugfix b/changelog.d/7533.bugfix new file mode 100644 index 0000000000..5e603ece22 --- /dev/null +++ b/changelog.d/7533.bugfix @@ -0,0 +1 @@ +Fix description of verified sessions From d89ef6988b6e894040da5e447cb781903b565767 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 11:35:50 +0100 Subject: [PATCH 105/679] Improve player seek --- .../MessageVoiceBroadcastListeningItem.kt | 7 +- .../VoiceBroadcastExtensions.kt | 5 + .../voicebroadcast/VoiceBroadcastHelper.kt | 4 +- .../listening/VoiceBroadcastPlayer.kt | 4 +- .../listening/VoiceBroadcastPlayerImpl.kt | 228 +++++++++--------- .../GetLiveVoiceBroadcastChunksUseCase.kt | 5 +- .../usecase/GetVoiceBroadcastEventUseCase.kt | 35 ++- 7 files changed, 157 insertions(+), 131 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 5e3cc6fba8..19caf3d8ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -71,18 +71,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } - seekBar.isEnabled = true } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } - seekBar.isEnabled = false - } - VoiceBroadcastPlayer.State.BUFFERING -> { - seekBar.isEnabled = true } + VoiceBroadcastPlayer.State.BUFFERING -> Unit } } } @@ -112,6 +108,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } is AudioMessagePlaybackTracker.Listener.State.Playing -> { if (!isUserSeeking) { +// Timber.d("Voice Broadcast | AudioMessagePlaybackTracker.Listener.onUpdate - duration: $duration, playbackTime: ${state.playbackTime}") holder.seekBar.progress = state.playbackTime } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index 48554f51d0..a1328c0ba3 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -17,6 +17,8 @@ package im.vector.app.features.voicebroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -34,3 +36,6 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? { val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 + +val VoiceBroadcastEvent.isLive + get() = content?.voiceBroadcastState != null && content?.voiceBroadcastState != VoiceBroadcastState.STOPPED diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 6839056520..3661928fa5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -49,8 +49,6 @@ class VoiceBroadcastHelper @Inject constructor( fun stopPlayback() = voiceBroadcastPlayer.stop() fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { - if (voiceBroadcastPlayer.currentVoiceBroadcast == voiceBroadcast) { - voiceBroadcastPlayer.seekTo(positionMillis) - } + voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis) } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index b4806ba57d..36e75236ad 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -46,9 +46,9 @@ interface VoiceBroadcastPlayer { fun stop() /** - * Seek to the given playback position, is milliseconds. + * Seek the given voice broadcast playback to the given position, is milliseconds. */ - fun seekTo(positionMillis: Int) + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) /** * Add a [Listener] to the given voice broadcast. diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index fc983e4112..773883d81a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -18,16 +18,18 @@ package im.vector.app.features.voicebroadcast.listening import android.media.AudioAttributes import android.media.MediaPlayer +import android.media.MediaPlayer.OnPreparedListener import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voicebroadcast.duration +import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcast -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer @@ -38,7 +40,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent @@ -47,6 +48,7 @@ import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.absoluteValue @Singleton class VoiceBroadcastPlayerImpl @Inject constructor( @@ -60,19 +62,20 @@ class VoiceBroadcastPlayerImpl @Inject constructor( get() = sessionHolder.getActiveSession() private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private var voiceBroadcastStateJob: Job? = null + private var fetchPlaylistTask: Job? = null + private var voiceBroadcastStateTask: Job? = null private val mediaPlayerListener = MediaPlayerListener() private val playbackTicker = PlaybackTicker() private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - private var currentSequence: Int? = null - private var fetchPlaylistJob: Job? = null private var playlist = emptyList() - - private var isLive: Boolean = false + private var currentSequence: Int? = null + private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null + private val isLive get() = currentVoiceBroadcastEvent?.isLive.orFalse() + private val lastSequence get() = currentVoiceBroadcastEvent?.content?.lastChunkSequence override var currentVoiceBroadcast: VoiceBroadcast? = null @@ -81,33 +84,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( set(value) { Timber.w("## VoiceBroadcastPlayer state: $field -> $value") field = value - // Notify state change to all the listeners attached to the current voice broadcast id - currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> - when (value) { - State.PLAYING -> { - playbackTracker.startPlayback(voiceBroadcastId) - playbackTicker.startPlaybackTicker(voiceBroadcastId) - } - State.PAUSED -> { - playbackTracker.pausePlayback(voiceBroadcastId) - playbackTicker.stopPlaybackTicker() - } - State.BUFFERING -> { - playbackTracker.pausePlayback(voiceBroadcastId) - playbackTicker.stopPlaybackTicker() - } - State.IDLE -> { - playbackTracker.stopPlayback(voiceBroadcastId) - playbackTicker.stopPlaybackTicker() - } - } - listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } - } + onPlayingStateChanged(value) } - /** - * Map voiceBroadcastId to listeners. - */ + /** Map voiceBroadcastId to listeners.*/ private val listeners: MutableMap> = mutableMapOf() override fun playOrResume(voiceBroadcast: VoiceBroadcast) { @@ -120,38 +100,28 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun pause() { - playingState = State.PAUSED currentMediaPlayer?.pause() + playingState = State.PAUSED } override fun stop() { // Update state playingState = State.IDLE - // Stop playback - currentMediaPlayer?.stop() - isLive = false - - // Release current player - release(currentMediaPlayer) - currentMediaPlayer = null - - // Release next player - release(nextMediaPlayer) - nextMediaPlayer = null - - // Do not observe anymore voice broadcast state changes - voiceBroadcastStateJob?.cancel() - voiceBroadcastStateJob = null + // Stop and release media players + stopPlayer() - // Do not fetch the playlist anymore - fetchPlaylistJob?.cancel() - fetchPlaylistJob = null + // Do not observe anymore voice broadcast changes + fetchPlaylistTask?.cancel() + fetchPlaylistTask = null + voiceBroadcastStateTask?.cancel() + voiceBroadcastStateTask = null // Clear playlist playlist = emptyList() currentSequence = null + currentVoiceBroadcastEvent = null currentVoiceBroadcast = null } @@ -174,13 +144,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.BUFFERING - val voiceBroadcastState = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content?.voiceBroadcastState - isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED + observeVoiceBroadcastLiveState(voiceBroadcast) fetchPlaylistAndStartPlayback(voiceBroadcast) } + private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { + voiceBroadcastStateTask = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + .onEach { currentVoiceBroadcastEvent = it.getOrNull() } + .launchIn(coroutineScope) + } + private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { - fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) + fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) .onEach(this::updatePlaylist) .launchIn(coroutineScope) } @@ -204,40 +179,51 @@ class VoiceBroadcastPlayerImpl @Inject constructor( when (playingState) { State.PLAYING -> { if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + prepareNextMediaPlayer() } } State.PAUSED -> { if (nextMediaPlayer == null) { - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + prepareNextMediaPlayer() } } State.BUFFERING -> { val newMediaContent = getNextAudioContent() - if (newMediaContent != null) startPlayback() + if (newMediaContent != null) { + val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } + startPlayback(savedPosition) + } + } + State.IDLE -> { + val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } + startPlayback(savedPosition) } - State.IDLE -> startPlayback() } } - private fun startPlayback(sequence: Int? = null, position: Int = 0) { + private fun startPlayback(position: Int? = null) { + stopPlayer() + val playlistItem = when { - sequence != null -> playlist.find { it.audioEvent.sequence == sequence } + position != null -> playlist.lastOrNull { it.startTime <= position } isLive -> playlist.lastOrNull() else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val computedSequence = playlistItem.audioEvent.sequence + val sequence = playlistItem.audioEvent.sequence + val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 coroutineScope.launch { try { - currentMediaPlayer = prepareMediaPlayer(content) - currentMediaPlayer?.start() - if (position > 0) { - currentMediaPlayer?.seekTo(position) + prepareMediaPlayer(content) { mp -> + currentMediaPlayer = mp + currentSequence = sequence + mp.start() + if (sequencePosition > 0) { + mp.seekTo(sequencePosition) + } + playingState = State.PLAYING + prepareNextMediaPlayer() } - currentSequence = computedSequence - withContext(Dispatchers.Main) { playingState = State.PLAYING } - nextMediaPlayer = prepareNextMediaPlayer() } catch (failure: Throwable) { Timber.e(failure, "Unable to start playback") throw VoiceFailure.UnableToPlay(failure) @@ -250,20 +236,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.PLAYING } - override fun seekTo(positionMillis: Int) { - val duration = getVoiceBroadcastDuration() - val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return - val audioEvent = playlistItem.audioEvent - val eventPosition = positionMillis - playlistItem.startTime - - Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${audioEvent.sequence}, sequencePosition=$eventPosition") - - tryOrNull { currentMediaPlayer?.stop() } - release(currentMediaPlayer) - tryOrNull { nextMediaPlayer?.stop() } - release(nextMediaPlayer) - - startPlayback(audioEvent.sequence, eventPosition) + override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { + if (voiceBroadcast != currentVoiceBroadcast) { + playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, 0f) + } else { + startPlayback(positionMillis) + } } private fun getNextAudioContent(): MessageAudioContent? { @@ -273,12 +251,24 @@ class VoiceBroadcastPlayerImpl @Inject constructor( return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content } - private suspend fun prepareNextMediaPlayer(): MediaPlayer? { - val nextContent = getNextAudioContent() ?: return null - return prepareMediaPlayer(nextContent) + private fun prepareNextMediaPlayer() { + nextMediaPlayer = null + val nextContent = getNextAudioContent() + if (nextContent != null) { + coroutineScope.launch { + prepareMediaPlayer(nextContent) { mp -> + if (nextMediaPlayer == null) { + nextMediaPlayer = mp + currentMediaPlayer?.setNextMediaPlayer(mp) + } else { + mp.release() + } + } + } + } } - private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer { + private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent, onPreparedListener: OnPreparedListener): MediaPlayer { // Download can fail val audioFile = try { session.fileService().downloadFile(messageAudioContent) @@ -299,57 +289,55 @@ class VoiceBroadcastPlayerImpl @Inject constructor( setDataSource(fis.fd) setOnInfoListener(mediaPlayerListener) setOnErrorListener(mediaPlayerListener) + setOnPreparedListener(onPreparedListener) setOnCompletionListener(mediaPlayerListener) prepare() } } } - private fun release(mp: MediaPlayer?) { - mp?.apply { - release() - setOnInfoListener(null) - setOnCompletionListener(null) - setOnErrorListener(null) + private fun stopPlayer() { + tryOrNull { currentMediaPlayer?.stop() } + currentMediaPlayer?.release() + currentMediaPlayer = null + + nextMediaPlayer?.release() + nextMediaPlayer = null + } + + private fun onPlayingStateChanged(playingState: State) { + // Notify state change to all the listeners attached to the current voice broadcast id + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> + when (playingState) { + State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId) + State.PAUSED, + State.BUFFERING, + State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) + } + listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) } } } private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, - MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { - release(currentMediaPlayer) - currentMediaPlayer = mp currentSequence = currentSequence?.plus(1) - coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } + currentMediaPlayer = mp + prepareNextMediaPlayer() } } return false } - override fun onPrepared(mp: MediaPlayer) { - when (mp) { - currentMediaPlayer -> { - nextMediaPlayer?.let { mp.setNextMediaPlayer(it) } - } - nextMediaPlayer -> { - tryOrNull { currentMediaPlayer?.setNextMediaPlayer(mp) } - } - } - } - override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - val voiceBroadcast = currentVoiceBroadcast ?: return - val voiceBroadcastEventContent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)?.content ?: return - isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED - if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) { + if (!isLive && lastSequence == currentSequence) { // We'll not receive new chunks anymore so we can stop the live listening stop() } else { @@ -388,23 +376,31 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (currentMediaPlayer?.isPlaying.orFalse()) { val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) + Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") if (currentVoiceBroadcastPosition != null) { val totalDuration = getVoiceBroadcastDuration() val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) } else { - playbackTracker.stopPlayback(id) - stopPlaybackTicker() + stopPlaybackTicker(id) } } else { - playbackTracker.stopPlayback(id) - stopPlaybackTicker() + stopPlaybackTicker(id) } } - fun stopPlaybackTicker() { + fun stopPlaybackTicker(id: String) { playbackTicker?.stop() playbackTicker = null + + val totalDuration = getVoiceBroadcastDuration() + val playbackTime = playbackTracker.getPlaybackTime(id) + val remainingTime = totalDuration - playbackTime + if (remainingTime < 1000) { + playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + } else { + playbackTracker.pausePlayback(id) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 2e8fc31870..33e370e9bc 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -29,9 +29,12 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.lastOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningReduce +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent @@ -57,7 +60,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) .mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } - val voiceBroadcastEvent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + val voiceBroadcastEvent = runBlocking { getVoiceBroadcastEventUseCase.execute(voiceBroadcast).firstOrNull()?.getOrNull() } val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index 26ba3209b7..7106322f06 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -16,12 +16,26 @@ package im.vector.app.features.voicebroadcast.usecase +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import javax.inject.Inject @@ -29,14 +43,27 @@ class GetVoiceBroadcastEventUseCase @Inject constructor( private val session: Session, ) { - fun execute(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { + fun execute(voiceBroadcast: VoiceBroadcast): Flow> { val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast") val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent() - val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) - .sortedBy { it.root.originServerTs } - return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent + val latestEvent = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) + .mapNotNull { it.root.asVoiceBroadcastEvent() } + .maxByOrNull { it.root.originServerTs ?: 0 } + ?: initialEvent + + return when (latestEvent?.content?.voiceBroadcastState) { + null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional()) + else -> { + room.flow() + .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty())) + .unwrap() + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.reference?.eventId == voiceBroadcast.voiceBroadcastId } + .map { it.toOptional() } + } + } } } From 392fe6fa329d84805f553986b8a338018ee2c095 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 15:47:10 +0100 Subject: [PATCH 106/679] Transform TickListener to fun interface --- .../vector/lib/attachmentviewer/VideoViewHolder.kt | 14 ++++++-------- .../im/vector/lib/core/utils/timer/CountUpTimer.kt | 2 +- .../vector/app/features/call/webrtc/WebRtcCall.kt | 10 ++++------ .../room/detail/composer/AudioMessageHelper.kt | 12 ++---------- .../composer/voice/VoiceMessageRecorderView.kt | 8 +++----- .../location/live/map/LiveLocationUserItem.kt | 6 ++---- .../listening/VoiceBroadcastPlayerImpl.kt | 6 +----- 7 files changed, 19 insertions(+), 39 deletions(-) diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt index 92d28d26c9..07c7b4588f 100644 --- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt +++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt @@ -103,14 +103,12 @@ class VideoViewHolder constructor(itemView: View) : views.videoView.setOnPreparedListener { stopTimer() countUpTimer = CountUpTimer(100).also { - it.tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - val duration = views.videoView.duration - val progress = views.videoView.currentPosition - val isPlaying = views.videoView.isPlaying -// Log.v("FOO", "isPlaying $isPlaying $progress/$duration") - eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) - } + it.tickListener = CountUpTimer.TickListener { + val duration = views.videoView.duration + val progress = views.videoView.currentPosition + val isPlaying = views.videoView.isPlaying + // Log.v("FOO", "isPlaying $isPlaying $progress/$duration") + eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } it.resume() } diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt index e9d311fe03..a4fd8bb4e1 100644 --- a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt @@ -66,7 +66,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) { coroutineScope.cancel() } - interface TickListener { + fun interface TickListener { fun onTick(milliseconds: Long) } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 00b9a76de7..0bf70690ba 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -167,12 +167,10 @@ class WebRtcCall( private var screenSender: RtpSender? = null private val timer = CountUpTimer(1000L).apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) - listeners.forEach { - tryOrNull { it.onTick(formattedDuration) } - } + tickListener = CountUpTimer.TickListener { milliseconds -> + val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) + listeners.forEach { + tryOrNull { it.onTick(formattedDuration) } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index bede02c17f..eddfe500b3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -199,11 +199,7 @@ class AudioMessageHelper @Inject constructor( private fun startRecordingAmplitudes() { amplitudeTicker?.stop() amplitudeTicker = CountUpTimer(50).apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - onAmplitudeTick() - } - } + tickListener = CountUpTimer.TickListener { onAmplitudeTick() } resume() } } @@ -234,11 +230,7 @@ class AudioMessageHelper @Inject constructor( private fun startPlaybackTicker(id: String) { playbackTicker?.stop() playbackTicker = CountUpTimer().apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - onPlaybackTick(id) - } - } + tickListener = CountUpTimer.TickListener { onPlaybackTick(id) } resume() } onPlaybackTick(id) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt index 13e0477ab6..a7b926f29a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt @@ -189,11 +189,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor( val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0) recordingTicker?.stop() recordingTicker = CountUpTimer().apply { - tickListener = object : CountUpTimer.TickListener { - override fun onTick(milliseconds: Long) { - val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked - onRecordingTick(isLocked, milliseconds + startMs) - } + tickListener = CountUpTimer.TickListener { milliseconds -> + val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked + onRecordingTick(isLocked, milliseconds + startMs) } resume() } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt index bab7f4c7f9..c108e83e76 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt @@ -79,10 +79,8 @@ abstract class LiveLocationUserItem : VectorEpoxyModel Date: Fri, 4 Nov 2022 15:54:28 +0100 Subject: [PATCH 107/679] VoiceBroadcastPlayerImpl - use session coroutine scope --- .../listening/VoiceBroadcastPlayerImpl.kt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index bf8ff11043..6eb9cbc735 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -22,6 +22,7 @@ import android.media.MediaPlayer.OnPreparedListener import androidx.annotation.MainThread import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker +import im.vector.app.features.session.coroutineScope import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.isLive @@ -33,10 +34,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -48,7 +46,6 @@ import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import javax.inject.Singleton -import kotlin.math.absoluteValue @Singleton class VoiceBroadcastPlayerImpl @Inject constructor( @@ -58,10 +55,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { - private val session - get() = sessionHolder.getActiveSession() + private val session get() = sessionHolder.getActiveSession() + private val sessionScope get() = session.coroutineScope - private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private var fetchPlaylistTask: Job? = null private var voiceBroadcastStateTask: Job? = null @@ -151,13 +147,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { voiceBroadcastStateTask = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) .onEach { currentVoiceBroadcastEvent = it.getOrNull() } - .launchIn(coroutineScope) + .launchIn(sessionScope) } private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) .onEach(this::updatePlaylist) - .launchIn(coroutineScope) + .launchIn(sessionScope) } private fun updatePlaylist(audioEvents: List) { @@ -212,7 +208,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } val sequence = playlistItem.audioEvent.sequence val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 - coroutineScope.launch { + sessionScope.launch { try { prepareMediaPlayer(content) { mp -> currentMediaPlayer = mp @@ -255,7 +251,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( nextMediaPlayer = null val nextContent = getNextAudioContent() if (nextContent != null) { - coroutineScope.launch { + sessionScope.launch { prepareMediaPlayer(nextContent) { mp -> if (nextMediaPlayer == null) { nextMediaPlayer = mp From c85b159952accaff1b7d65f027ced44fe790f0e4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 17:12:02 +0100 Subject: [PATCH 108/679] VoiceBroadcastPlayer - Extract some code to VoiceBroadcastPlaylist --- .../VoiceBroadcastExtensions.kt | 7 +- .../listening/VoiceBroadcastPlayerImpl.kt | 60 +++++------------ .../listening/VoiceBroadcastPlaylist.kt | 67 +++++++++++++++++++ 3 files changed, 91 insertions(+), 43 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index a1328c0ba3..fa8033a211 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -16,9 +16,11 @@ package im.vector.app.features.voicebroadcast +import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -38,4 +40,7 @@ val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 val VoiceBroadcastEvent.isLive - get() = content?.voiceBroadcastState != null && content?.voiceBroadcastState != VoiceBroadcastState.STOPPED + get() = content?.isLive.orFalse() + +val MessageVoiceBroadcastInfoContent.isLive + get() = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 6eb9cbc735..de4f965a59 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -24,7 +24,6 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.session.coroutineScope import im.vector.app.features.voice.VoiceFailure -import im.vector.app.features.voicebroadcast.duration import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State @@ -41,7 +40,6 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent -import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -67,11 +65,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - private var playlist = emptyList() - private var currentSequence: Int? = null + private val playlist = VoiceBroadcastPlaylist() private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null - private val isLive get() = currentVoiceBroadcastEvent?.isLive.orFalse() - private val lastSequence get() = currentVoiceBroadcastEvent?.content?.lastChunkSequence override var currentVoiceBroadcast: VoiceBroadcast? = null @@ -114,8 +109,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( voiceBroadcastStateTask = null // Clear playlist - playlist = emptyList() - currentSequence = null + playlist.reset() currentVoiceBroadcastEvent = null currentVoiceBroadcast = null @@ -152,25 +146,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) - .onEach(this::updatePlaylist) + .onEach { + playlist.setItems(it) + onPlaylistUpdated() + } .launchIn(sessionScope) } - private fun updatePlaylist(audioEvents: List) { - val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } - val chunkPositions = sorted - .map { it.duration } - .runningFold(0) { acc, i -> acc + i } - .dropLast(1) - playlist = sorted.mapIndexed { index, messageAudioEvent -> - PlaylistItem( - audioEvent = messageAudioEvent, - startTime = chunkPositions.getOrNull(index) ?: 0 - ) - } - onPlaylistUpdated() - } - private fun onPlaylistUpdated() { when (playingState) { State.PLAYING -> { @@ -201,18 +183,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( stopPlayer() val playlistItem = when { - position != null -> playlist.lastOrNull { it.startTime <= position } - isLive -> playlist.lastOrNull() + position != null -> playlist.findByPosition(position) + currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull() else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = playlistItem.audioEvent.sequence + val sequence = playlistItem.audioEvent.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return } val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 sessionScope.launch { try { prepareMediaPlayer(content) { mp -> currentMediaPlayer = mp - currentSequence = sequence + playlist.currentSequence = sequence mp.start() if (sequencePosition > 0) { mp.seekTo(sequencePosition) @@ -241,10 +223,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun getNextAudioContent(): MessageAudioContent? { - val nextSequence = currentSequence?.plus(1) - ?: playlist.lastOrNull()?.audioEvent?.sequence - ?: 1 - return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content + return playlist.getNextItem()?.audioEvent?.content } private fun prepareNextMediaPlayer() { @@ -322,7 +301,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { - currentSequence = currentSequence?.plus(1) + playlist.currentSequence++ currentMediaPlayer = mp prepareNextMediaPlayer() } @@ -333,7 +312,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - if (!isLive && lastSequence == currentSequence) { + val content = currentVoiceBroadcastEvent?.content + val isLive = content?.isLive.orFalse() + if (!isLive && content?.lastChunkSequence == playlist.currentSequence) { // We'll not receive new chunks anymore so we can stop the live listening stop() } else { @@ -347,10 +328,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 - - private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) - private inner class PlaybackTicker( private var playbackTicker: CountUpTimer? = null, ) { @@ -366,12 +343,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaybackTick(id: String) { if (currentMediaPlayer?.isPlaying.orFalse()) { - val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime + val itemStartPosition = playlist.currentItem?.startTime val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") if (currentVoiceBroadcastPosition != null) { - val totalDuration = getVoiceBroadcastDuration() - val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration + val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) } else { stopPlaybackTicker(id) @@ -385,7 +361,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTicker?.stop() playbackTicker = null - val totalDuration = getVoiceBroadcastDuration() + val totalDuration = playlist.duration val playbackTime = playbackTracker.getPlaybackTime(id) val remainingTime = totalDuration - playbackTime if (remainingTime < 1000) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt new file mode 100644 index 0000000000..5cd6efc28a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.listening + +import im.vector.app.features.voicebroadcast.duration +import im.vector.app.features.voicebroadcast.sequence +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent + +class VoiceBroadcastPlaylist( + private val items: MutableList = mutableListOf(), +) : List by items { + + var currentSequence = 1 + val currentItem get() = findBySequence(currentSequence) + + val duration + get() = items.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 + + fun setItems(audioEvents: List) { + items.clear() + val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } + val chunkPositions = sorted + .map { it.duration } + .runningFold(0) { acc, i -> acc + i } + .dropLast(1) + val newItems = sorted.mapIndexed { index, messageAudioEvent -> + PlaylistItem( + audioEvent = messageAudioEvent, + startTime = chunkPositions.getOrNull(index) ?: 0 + ) + } + items.addAll(newItems) + } + + fun reset() { + currentSequence = 1 + items.clear() + } + + fun findByPosition(positionMillis: Int): PlaylistItem? { + return items.lastOrNull { it.startTime <= positionMillis } + } + + fun findBySequence(sequenceNumber: Int): PlaylistItem? { + return items.find { it.audioEvent.sequence == sequenceNumber } + } + + fun getNextItem() = findBySequence(currentSequence.plus(1)) + + fun firstOrNull() = findBySequence(1) +} + +data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) From 37c75354bed4a884c03c2ca15a6f22e32d05f572 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:18:26 +0100 Subject: [PATCH 109/679] VoiceBroadcastPlayer - Reorganize some code --- .../listening/VoiceBroadcastPlayerImpl.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index de4f965a59..a38442a19a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -56,16 +56,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private val session get() = sessionHolder.getActiveSession() private val sessionScope get() = session.coroutineScope - private var fetchPlaylistTask: Job? = null - private var voiceBroadcastStateTask: Job? = null - private val mediaPlayerListener = MediaPlayerListener() private val playbackTicker = PlaybackTicker() + private val playlist = VoiceBroadcastPlaylist() + + private var fetchPlaylistTask: Job? = null + private var voiceBroadcastStateObserver: Job? = null private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - private val playlist = VoiceBroadcastPlaylist() private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null override var currentVoiceBroadcast: VoiceBroadcast? = null @@ -105,8 +105,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Do not observe anymore voice broadcast changes fetchPlaylistTask?.cancel() fetchPlaylistTask = null - voiceBroadcastStateTask?.cancel() - voiceBroadcastStateTask = null + voiceBroadcastStateObserver?.cancel() + voiceBroadcastStateObserver = null // Clear playlist playlist.reset() @@ -139,7 +139,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { - voiceBroadcastStateTask = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) .onEach { currentVoiceBroadcastEvent = it.getOrNull() } .launchIn(sessionScope) } @@ -345,7 +345,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (currentMediaPlayer?.isPlaying.orFalse()) { val itemStartPosition = playlist.currentItem?.startTime val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) - Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") + Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") if (currentVoiceBroadcastPosition != null) { val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) From b87b2cbb63aea881d753759482167e89f0c5ca0c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:28:26 +0100 Subject: [PATCH 110/679] Remove useless method --- .../listening/VoiceBroadcastPlayerImpl.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index a38442a19a..de01661431 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -166,8 +166,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } State.BUFFERING -> { - val newMediaContent = getNextAudioContent() - if (newMediaContent != null) { + val nextItem = playlist.getNextItem() + if (nextItem != null) { val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } startPlayback(savedPosition) } @@ -222,14 +222,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun getNextAudioContent(): MessageAudioContent? { - return playlist.getNextItem()?.audioEvent?.content - } - private fun prepareNextMediaPlayer() { nextMediaPlayer = null - val nextContent = getNextAudioContent() - if (nextContent != null) { + val nextItem = playlist.getNextItem() + if (nextItem != null) { sessionScope.launch { prepareMediaPlayer(nextContent) { mp -> if (nextMediaPlayer == null) { From a3cd861e15d09ec29ae29f84b3586523647c22e6 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:29:01 +0100 Subject: [PATCH 111/679] Add isPreparingNextPlayer flag --- .../listening/VoiceBroadcastPlayerImpl.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index de01661431..adc1912140 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -65,6 +65,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null + private var isPreparingNextPlayer: Boolean = false private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null @@ -156,12 +157,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaylistUpdated() { when (playingState) { State.PLAYING -> { - if (nextMediaPlayer == null) { + if (nextMediaPlayer == null && !isPreparingNextPlayer) { prepareNextMediaPlayer() } } State.PAUSED -> { - if (nextMediaPlayer == null) { + if (nextMediaPlayer == null && !isPreparingNextPlayer) { prepareNextMediaPlayer() } } @@ -223,17 +224,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun prepareNextMediaPlayer() { - nextMediaPlayer = null val nextItem = playlist.getNextItem() if (nextItem != null) { + isPreparingNextPlayer = true sessionScope.launch { - prepareMediaPlayer(nextContent) { mp -> - if (nextMediaPlayer == null) { - nextMediaPlayer = mp - currentMediaPlayer?.setNextMediaPlayer(mp) - } else { - mp.release() - } + prepareMediaPlayer(nextItem.audioEvent.content) { mp -> + nextMediaPlayer = mp + currentMediaPlayer?.setNextMediaPlayer(mp) + isPreparingNextPlayer = false } } } @@ -274,6 +272,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( nextMediaPlayer?.release() nextMediaPlayer = null + isPreparingNextPlayer = false } private fun onPlayingStateChanged(playingState: State) { From a3201555462586c837f883216170668e2ed7840a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:36:37 +0100 Subject: [PATCH 112/679] reset nextMediaPlayer when item has changed --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index adc1912140..da3a9559a4 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -298,6 +298,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { playlist.currentSequence++ currentMediaPlayer = mp + nextMediaPlayer = null prepareNextMediaPlayer() } } From 43a112839f6bcd2bfb17dc3bf36c16f92cd3d58d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 18:49:10 +0100 Subject: [PATCH 113/679] Fix seek when playlist is not loaded --- .../listening/VoiceBroadcastPlayerImpl.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index da3a9559a4..7b8d8c9547 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -358,12 +358,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTicker = null val totalDuration = playlist.duration - val playbackTime = playbackTracker.getPlaybackTime(id) - val remainingTime = totalDuration - playbackTime - if (remainingTime < 1000) { - playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) - } else { - playbackTracker.pausePlayback(id) + if (totalDuration > 0) { + val playbackTime = playbackTracker.getPlaybackTime(id) + val remainingTime = totalDuration - playbackTime + if (remainingTime < 1000) { + playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + } else { + playbackTracker.pausePlayback(id) + } } } } From 266236c1e5daee19bdb6e632ab74850a30ed3c7e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:20:22 +0100 Subject: [PATCH 114/679] set playlist.currentSequence null by default --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- .../voicebroadcast/listening/VoiceBroadcastPlaylist.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 7b8d8c9547..921e0e69ea 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -296,7 +296,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { - playlist.currentSequence++ + playlist.currentSequence = playlist.currentSequence?.inc() currentMediaPlayer = mp nextMediaPlayer = null prepareNextMediaPlayer() diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt index 5cd6efc28a..ff388c2313 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -24,8 +24,8 @@ class VoiceBroadcastPlaylist( private val items: MutableList = mutableListOf(), ) : List by items { - var currentSequence = 1 - val currentItem get() = findBySequence(currentSequence) + var currentSequence: Int? = null + val currentItem get() = currentSequence?.let { findBySequence(it) } val duration get() = items.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 @@ -47,7 +47,7 @@ class VoiceBroadcastPlaylist( } fun reset() { - currentSequence = 1 + currentSequence = null items.clear() } @@ -59,7 +59,7 @@ class VoiceBroadcastPlaylist( return items.find { it.audioEvent.sequence == sequenceNumber } } - fun getNextItem() = findBySequence(currentSequence.plus(1)) + fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1) fun firstOrNull() = findBySequence(1) } From a47e3c1233999b8e5ebbd83974626b86d58488a0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:20:54 +0100 Subject: [PATCH 115/679] Improve playing state updates --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 921e0e69ea..bfbc53fd78 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -74,9 +74,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override var playingState = State.IDLE @MainThread set(value) { - Timber.w("## VoiceBroadcastPlayer state: $field -> $value") - field = value - onPlayingStateChanged(value) + if (field != value) { + Timber.w("## VoiceBroadcastPlayer state: $field -> $value") + field = value + onPlayingStateChanged(value) + } } /** Map voiceBroadcastId to listeners.*/ @@ -299,6 +301,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playlist.currentSequence = playlist.currentSequence?.inc() currentMediaPlayer = mp nextMediaPlayer = null + playingState = State.PLAYING prepareNextMediaPlayer() } } From b2f35fa1352f1b3771d8d3aa9231340560bebdab Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:21:18 +0100 Subject: [PATCH 116/679] Improve PlaybackTicker --- .../helper/AudioMessagePlaybackTracker.kt | 2 +- .../listening/VoiceBroadcastPlayerImpl.kt | 51 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index 6937cd3a46..7e40b92ac8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -127,7 +127,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { } } - private fun getPercentage(id: String): Float { + fun getPercentage(id: String): Float { return when (val state = states[id]) { is Listener.State.Playing -> state.percentage is Listener.State.Paused -> state.percentage diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index bfbc53fd78..50a3002c0d 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -340,34 +340,37 @@ class VoiceBroadcastPlayerImpl @Inject constructor( onPlaybackTick(id) } - private fun onPlaybackTick(id: String) { - if (currentMediaPlayer?.isPlaying.orFalse()) { - val itemStartPosition = playlist.currentItem?.startTime - val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) - Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") - if (currentVoiceBroadcastPosition != null) { - val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration - playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) - } else { - stopPlaybackTicker(id) - } - } else { - stopPlaybackTicker(id) - } - } - fun stopPlaybackTicker(id: String) { playbackTicker?.stop() playbackTicker = null + onPlaybackTick(id) + } - val totalDuration = playlist.duration - if (totalDuration > 0) { - val playbackTime = playbackTracker.getPlaybackTime(id) - val remainingTime = totalDuration - playbackTime - if (remainingTime < 1000) { - playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) - } else { - playbackTracker.pausePlayback(id) + private fun onPlaybackTick(id: String) { + val currentItem = playlist.currentItem ?: return + val itemStartTime = currentItem.startTime + val duration = playlist.duration + when (playingState) { + State.PLAYING, + State.PAUSED -> { + Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartTime $itemStartTime, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") + val position = itemStartTime + (currentMediaPlayer?.currentPosition ?: 0) + val percentage = position.toFloat() / playlist.duration + if (playingState == State.PLAYING) { + playbackTracker.updatePlayingAtPlaybackTime(id, position, percentage) + } else { + playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) + } + } + State.BUFFERING, + State.IDLE -> { + val playbackTime = playbackTracker.getPlaybackTime(id) + val percentage = playbackTracker.getPercentage(id) + if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 1000) { + playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + } else { + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } } } } From 436e76c7563f3f529f395630c75dd59ef32436aa Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:42:02 +0100 Subject: [PATCH 117/679] Fix seek on paused state --- .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/TimelineViewModel.kt | 2 +- .../MessageVoiceBroadcastListeningItem.kt | 2 +- .../voicebroadcast/VoiceBroadcastHelper.kt | 4 +- .../listening/VoiceBroadcastPlayer.kt | 2 +- .../listening/VoiceBroadcastPlayerImpl.kt | 40 ++++++++++++++----- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index ba0f7dbdf8..faee8f652c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -133,7 +133,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening() object Pause : Listening() object Stop : Listening() - data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int) : Listening() + data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int, val duration: Int) : Listening() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 252823b2a6..ef238d56e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -637,7 +637,7 @@ class TimelineViewModel @AssistedInject constructor( is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast) VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() - is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis) + is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis, action.duration) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 19caf3d8ba..ebbfe13730 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -94,7 +94,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } override fun onStopTrackingTouch(seekBar: SeekBar) { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress)) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration)) isUserSeeking = false } }) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 3661928fa5..38fb157748 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -48,7 +48,7 @@ class VoiceBroadcastHelper @Inject constructor( fun stopPlayback() = voiceBroadcastPlayer.stop() - fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { - voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis) + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) { + voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis, duration) } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 36e75236ad..8c11db4f43 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -48,7 +48,7 @@ interface VoiceBroadcastPlayer { /** * Seek the given voice broadcast playback to the given position, is milliseconds. */ - fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) + fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) /** * Add a [Listener] to the given voice broadcast. diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 50a3002c0d..0937977b70 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -94,8 +94,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun pause() { - currentMediaPlayer?.pause() - playingState = State.PAUSED + pausePlayback() } override fun stop() { @@ -212,16 +211,39 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun resumePlayback() { - currentMediaPlayer?.start() - playingState = State.PLAYING + private fun pausePlayback(positionMillis: Int? = null) { + if (positionMillis == null) { + currentMediaPlayer?.pause() + } else { + stopPlayer() + currentVoiceBroadcast?.voiceBroadcastId?.let { id -> + playbackTracker.updatePausedAtPlaybackTime(id, positionMillis, positionMillis.toFloat() / playlist.duration) + } + } + playingState = State.PAUSED } - override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int) { - if (voiceBroadcast != currentVoiceBroadcast) { - playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, 0f) + private fun resumePlayback() { + if (currentMediaPlayer != null) { + currentMediaPlayer?.start() + playingState = State.PLAYING } else { - startPlayback(positionMillis) + val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } + startPlayback(position) + } + } + + override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) { + when { + voiceBroadcast != currentVoiceBroadcast -> { + playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) + } + playingState == State.PLAYING || playingState == State.BUFFERING -> { + startPlayback(positionMillis) + } + playingState == State.IDLE || playingState == State.PAUSED -> { + pausePlayback(positionMillis) + } } } From 7d51a265222c5bcabee7fe3e92a7a956e6c37377 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 23:45:38 +0100 Subject: [PATCH 118/679] Decrease tick interval --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 0937977b70..f7e296ffed 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -355,7 +355,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( fun startPlaybackTicker(id: String) { playbackTicker?.stop() - playbackTicker = CountUpTimer().apply { + playbackTicker = CountUpTimer(50L).apply { tickListener = CountUpTimer.TickListener { onPlaybackTick(id) } resume() } @@ -388,7 +388,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( State.IDLE -> { val playbackTime = playbackTracker.getPlaybackTime(id) val percentage = playbackTracker.getPercentage(id) - if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 1000) { + if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 100) { playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) } else { playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) From baa9cb39b0ff0755376b612e7d5c10fdcab439cb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Sat, 5 Nov 2022 00:06:00 +0100 Subject: [PATCH 119/679] Fix broken live listening --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index f7e296ffed..b613d99d33 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -171,12 +171,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val nextItem = playlist.getNextItem() if (nextItem != null) { val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } - startPlayback(savedPosition) + startPlayback(savedPosition?.takeIf { it > 0 }) } } State.IDLE -> { val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } - startPlayback(savedPosition) + startPlayback(savedPosition?.takeIf { it > 0 }) } } } @@ -389,7 +389,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val playbackTime = playbackTracker.getPlaybackTime(id) val percentage = playbackTracker.getPercentage(id) if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 100) { - playbackTracker.updatePausedAtPlaybackTime(id, 0, 0f) + playbackTracker.stopPlayback(id) } else { playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) } From c5e6eb0d0e131f86ba9da3af94fa8c0455ebbc6e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Sat, 5 Nov 2022 00:07:02 +0100 Subject: [PATCH 120/679] Remove some logs --- .../detail/timeline/item/MessageVoiceBroadcastListeningItem.kt | 1 - .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index ebbfe13730..bdd0670029 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -108,7 +108,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } is AudioMessagePlaybackTracker.Listener.State.Playing -> { if (!isUserSeeking) { -// Timber.d("Voice Broadcast | AudioMessagePlaybackTracker.Listener.onUpdate - duration: $duration, playbackTime: ${state.playbackTime}") holder.seekBar.progress = state.playbackTime } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index b613d99d33..9fb7c3ccb5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -375,7 +375,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( when (playingState) { State.PLAYING, State.PAUSED -> { - Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: ${playlist.currentSequence}, itemStartTime $itemStartTime, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") val position = itemStartTime + (currentMediaPlayer?.currentPosition ?: 0) val percentage = position.toFloat() / playlist.duration if (playingState == State.PLAYING) { From aa8eec221a1551b6d737e6a58feffe8f82fd3da7 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Sat, 5 Nov 2022 00:29:11 +0100 Subject: [PATCH 121/679] Enable fast backward/forward buttons --- .../MessageVoiceBroadcastListeningItem.kt | 59 +++++++++++++------ .../listening/VoiceBroadcastPlayerImpl.kt | 13 ++-- ...e_event_voice_broadcast_listening_stub.xml | 4 +- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index bdd0670029..558f81b5fa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -28,6 +28,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @@ -48,6 +49,32 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) + bindButtons(holder) + } + + private fun bindButtons(holder: Holder) { + with(holder) { + playPauseButton.onClick { + when (player.playingState) { + VoiceBroadcastPlayer.State.PLAYING -> { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) + } + VoiceBroadcastPlayer.State.PAUSED, + VoiceBroadcastPlayer.State.IDLE -> { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) + } + VoiceBroadcastPlayer.State.BUFFERING -> Unit + } + } + fastBackwardButton.onClick { + val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) + } + fastForwardButton.onClick { + val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration) + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) + } + } } override fun renderMetadata(holder: Holder) { @@ -63,20 +90,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING - fastBackwardButton.isInvisible = true - fastForwardButton.isInvisible = true - when (state) { VoiceBroadcastPlayer.State.PLAYING -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) } } VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.PAUSED -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) - playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } VoiceBroadcastPlayer.State.BUFFERING -> Unit } @@ -99,25 +121,24 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } }) playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { - when (state) { - is AudioMessagePlaybackTracker.Listener.State.Paused -> { - if (!isUserSeeking) { - holder.seekBar.progress = state.playbackTime - } - } - is AudioMessagePlaybackTracker.Listener.State.Playing -> { - if (!isUserSeeking) { - holder.seekBar.progress = state.playbackTime - } - } - AudioMessagePlaybackTracker.Listener.State.Idle -> Unit - is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit + override fun onUpdate(state: State) { + renderBackwardForwardButtons(holder, state) + if (!isUserSeeking) { + holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) } } }) } + private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) { + val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused + val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) + val canBackward = isPlayingOrPaused && playbackTime > 0 + val canForward = isPlayingOrPaused && playbackTime < duration + holder.fastBackwardButton.isInvisible = !canBackward + holder.fastForwardButton.isInvisible = !canForward + } + private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 9fb7c3ccb5..020edc283a 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -371,7 +371,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaybackTick(id: String) { val currentItem = playlist.currentItem ?: return val itemStartTime = currentItem.startTime - val duration = playlist.duration when (playingState) { State.PLAYING, State.PAUSED -> { @@ -383,15 +382,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) } } - State.BUFFERING, - State.IDLE -> { + State.BUFFERING -> { val playbackTime = playbackTracker.getPlaybackTime(id) val percentage = playbackTracker.getPercentage(id) - if (playingState == State.IDLE && duration > 0 && (duration - playbackTime) < 100) { - playbackTracker.stopPlayback(id) - } else { - playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) - } + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } + State.IDLE -> { + playbackTracker.stopPlayback(id) } } } diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index bed9407dfa..150f1cb281 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -100,7 +100,7 @@ android:id="@+id/fastBackwardButton" android:layout_width="24dp" android:layout_height="24dp" - android:background="@android:color/transparent" + android:background="@drawable/bg_rounded_button" android:contentDescription="@string/a11y_voice_broadcast_fast_backward" android:src="@drawable/ic_player_backward_30" app:tint="?vctr_content_secondary" /> @@ -127,7 +127,7 @@ android:id="@+id/fastForwardButton" android:layout_width="24dp" android:layout_height="24dp" - android:background="@android:color/transparent" + android:background="@drawable/bg_rounded_button" android:contentDescription="@string/a11y_voice_broadcast_fast_forward" android:src="@drawable/ic_player_forward_30" app:tint="?vctr_content_secondary" /> From 1c40f9c5e82ca4128238a42c99652954c787d33e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 11:40:57 +0100 Subject: [PATCH 122/679] Minor cleanup --- .../MessageVoiceBroadcastListeningItem.kt | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 558f81b5fa..a43783a626 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -44,9 +44,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - playerListener = VoiceBroadcastPlayer.Listener { state -> - renderPlayingState(holder, state) - } + playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) } player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) bindButtons(holder) @@ -56,13 +54,9 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem with(holder) { playPauseButton.onClick { when (player.playingState) { - VoiceBroadcastPlayer.State.PLAYING -> { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) - } + VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) VoiceBroadcastPlayer.State.PAUSED, - VoiceBroadcastPlayer.State.IDLE -> { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) - } + VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) VoiceBroadcastPlayer.State.BUFFERING -> Unit } } @@ -106,20 +100,22 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindSeekBar(holder: Holder) { - holder.durationView.text = formatPlaybackTime(duration) - holder.seekBar.max = duration - holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit + with(holder) { + durationView.text = formatPlaybackTime(duration) + seekBar.max = duration + seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit - override fun onStartTrackingTouch(seekBar: SeekBar) { - isUserSeeking = true - } + override fun onStartTrackingTouch(seekBar: SeekBar) { + isUserSeeking = true + } - override fun onStopTrackingTouch(seekBar: SeekBar) { - callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration)) - isUserSeeking = false - } - }) + override fun onStopTrackingTouch(seekBar: SeekBar) { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration)) + isUserSeeking = false + } + }) + } playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { override fun onUpdate(state: State) { renderBackwardForwardButtons(holder, state) From 226e2026a12452884b05434815afe609e671a98d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 11:42:04 +0100 Subject: [PATCH 123/679] Remove item listeners --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index a43783a626..c83b1f6954 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -140,8 +140,13 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem override fun unbind(holder: Holder) { super.unbind(holder) player.removeListener(voiceBroadcast, playerListener) - holder.seekBar.setOnSeekBarChangeListener(null) playbackTracker.untrack(voiceBroadcast.voiceBroadcastId) + with(holder) { + seekBar.onClick(null) + playPauseButton.onClick(null) + fastForwardButton.onClick(null) + fastBackwardButton.onClick(null) + } } override fun getViewStubId() = STUB_ID From 6b57b1190cccbe8e73e8e5b3934c14ad399b57eb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 11:46:30 +0100 Subject: [PATCH 124/679] Make AudioMessagePlaybackTracker.Listener interface funny --- .../helper/AudioMessagePlaybackTracker.kt | 2 +- .../detail/timeline/item/MessageAudioItem.kt | 16 +++++++--------- .../item/MessageVoiceBroadcastListeningItem.kt | 12 +++++------- .../detail/timeline/item/MessageVoiceItem.kt | 16 +++++++--------- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index 7e40b92ac8..91f27ce5a8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -148,7 +148,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { const val RECORDING_ID = "RECORDING_ID" } - interface Listener { + fun interface Listener { fun onUpdate(state: State) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt index fda9a1465f..3e8d6cb487 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt @@ -140,16 +140,14 @@ abstract class MessageAudioItem : AbsMessageItem() { } private fun renderStateBasedOnAudioPlayback(holder: Holder) { - audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { - when (state) { - is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) - is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) - is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) - is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit - } + audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state -> + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) + is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) + is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) + is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit } - }) + } } private fun renderIdleState(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index c83b1f6954..1076e06b7e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -116,14 +116,12 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } }) } - playbackTracker.track(voiceBroadcast.voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: State) { - renderBackwardForwardButtons(holder, state) - if (!isUserSeeking) { - holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) - } + playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState -> + renderBackwardForwardButtons(holder, playbackState) + if (!isUserSeeking) { + holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) } - }) + } } private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index e057950790..d3f320db7d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -122,16 +122,14 @@ abstract class MessageVoiceItem : AbsMessageItem() { true } - audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener { - override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { - when (state) { - is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed) - is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed) - is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed) - is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit - } + audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state -> + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed) + is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed) + is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed) + is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit } - }) + } } private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f) From 305a362e9ee15e6a915053a5457d70e6de087dc8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 12:04:30 +0100 Subject: [PATCH 125/679] Fix play action on other voice broadcast than the current one --- .../item/MessageVoiceBroadcastListeningItem.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 1076e06b7e..4b91bbfb0e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -27,7 +27,6 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction -import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @@ -53,11 +52,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem private fun bindButtons(holder: Holder) { with(holder) { playPauseButton.onClick { - when (player.playingState) { - VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) - VoiceBroadcastPlayer.State.PAUSED, - VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) - VoiceBroadcastPlayer.State.BUFFERING -> Unit + if (player.currentVoiceBroadcast == voiceBroadcast) { + when (player.playingState) { + VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) + VoiceBroadcastPlayer.State.PAUSED, + VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) + VoiceBroadcastPlayer.State.BUFFERING -> Unit + } + } else { + callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } fastBackwardButton.onClick { From be18f4ec78af8bb77f3ff99f65157f3c12aaca35 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 14:09:15 +0100 Subject: [PATCH 126/679] remove unused imports --- .../listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt | 1 - .../voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 33e370e9bc..d12a329142 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.lastOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.runningReduce import kotlinx.coroutines.runBlocking diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index 7106322f06..696d300fc3 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -22,9 +22,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -56,7 +54,7 @@ class GetVoiceBroadcastEventUseCase @Inject constructor( return when (latestEvent?.content?.voiceBroadcastState) { null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional()) - else -> { + else -> { room.flow() .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty())) .unwrap() From 9e83d88f08ae492a2f1271f0df255edf66c546c4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 14:52:53 +0100 Subject: [PATCH 127/679] Fix seek position when listening another voice broadcast --- .../listening/VoiceBroadcastPlayerImpl.kt | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 020edc283a..977b3906e0 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -216,8 +216,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( currentMediaPlayer?.pause() } else { stopPlayer() - currentVoiceBroadcast?.voiceBroadcastId?.let { id -> - playbackTracker.updatePausedAtPlaybackTime(id, positionMillis, positionMillis.toFloat() / playlist.duration) + val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId + val duration = playlist.duration.takeIf { it > 0 } + if (voiceBroadcastId != null && duration != null) { + playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) } } playingState = State.PAUSED @@ -312,6 +314,22 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } + private fun getCurrentPlaybackPosition(): Int? { + val playlistPosition = playlist.currentItem?.startTime + val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition + val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } + return computedPosition ?: savedPosition + } + + private fun getCurrentPlaybackPercentage(): Float? { + val playlistPosition = playlist.currentItem?.startTime + val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition + val duration = playlist.duration.takeIf { it > 0 } + val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null + val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) } + return computedPercentage ?: savedPercentage + } + private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, @@ -369,26 +387,26 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun onPlaybackTick(id: String) { - val currentItem = playlist.currentItem ?: return - val itemStartTime = currentItem.startTime + val playbackTime = getCurrentPlaybackPosition() + val percentage = getCurrentPlaybackPercentage() when (playingState) { - State.PLAYING, - State.PAUSED -> { - val position = itemStartTime + (currentMediaPlayer?.currentPosition ?: 0) - val percentage = position.toFloat() / playlist.duration - if (playingState == State.PLAYING) { - playbackTracker.updatePlayingAtPlaybackTime(id, position, percentage) - } else { - playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) + State.PLAYING -> { + if (playbackTime != null && percentage != null) { + playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage) } } + State.PAUSED, State.BUFFERING -> { - val playbackTime = playbackTracker.getPlaybackTime(id) - val percentage = playbackTracker.getPercentage(id) - playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + if (playbackTime != null && percentage != null) { + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } } State.IDLE -> { - playbackTracker.stopPlayback(id) + if (playbackTime == null || percentage == null || playbackTime == playlist.duration) { + playbackTracker.stopPlayback(id) + } else { + playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) + } } } } From 4e533667278c5a5f6900edebc58c5d6cad4c7725 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 7 Nov 2022 15:46:52 +0100 Subject: [PATCH 128/679] Fix default visibility of fast backward/forward buttons --- ...timeline_event_voice_broadcast_listening_stub.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 150f1cb281..1d31afba99 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -103,7 +103,9 @@ android:background="@drawable/bg_rounded_button" android:contentDescription="@string/a11y_voice_broadcast_fast_backward" android:src="@drawable/ic_player_backward_30" - app:tint="?vctr_content_secondary" /> + android:visibility="invisible" + app:tint="?vctr_content_secondary" + tools:visibility="visible" /> + android:indeterminateTint="?vctr_content_secondary" + android:visibility="gone" + tools:visibility="visible" /> + android:visibility="invisible" + app:tint="?vctr_content_secondary" + tools:visibility="visible" /> Date: Mon, 7 Nov 2022 16:05:06 +0100 Subject: [PATCH 129/679] improve end of voice broadcast check --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 977b3906e0..6a6dc6a9e8 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -402,7 +402,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } State.IDLE -> { - if (playbackTime == null || percentage == null || playbackTime == playlist.duration) { + if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) { playbackTracker.stopPlayback(id) } else { playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) From 0c40acb186efad47fc19b924eae4320f88282c0a Mon Sep 17 00:00:00 2001 From: NIkita Fedrunov Date: Mon, 7 Nov 2022 16:16:51 +0100 Subject: [PATCH 130/679] temporary workaround for a failing sync due to unexpected `enableUnreadThreadNotifications` param --- .../android/sdk/internal/session/filter/FilterFactory.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt index 77c5649709..e0919c52e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt @@ -67,7 +67,9 @@ internal object FilterFactory { } private fun createElementTimelineFilter(): RoomEventFilter? { - return RoomEventFilter(enableUnreadThreadNotifications = true) +// we need to check if homeserver supports thread notifications before setting this param +// return RoomEventFilter(enableUnreadThreadNotifications = true) + return null } private fun createElementStateFilter(): RoomEventFilter { From 456762a464f5b959508a6b3d9883ebc0a33e5890 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 7 Nov 2022 18:26:54 +0300 Subject: [PATCH 131/679] Add toggle ip address menu option. --- .../src/main/res/values/strings.xml | 2 ++ .../settings/devices/v2/DevicesAction.kt | 1 + .../settings/devices/v2/DevicesViewModel.kt | 6 ++++++ .../settings/devices/v2/DevicesViewState.kt | 1 + .../v2/VectorSettingsDevicesFragment.kt | 19 ++++++++++++++++--- .../res/menu/menu_other_sessions_header.xml | 5 +++++ 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index cd7cb3f477..0292847f0b 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3350,6 +3350,8 @@ Sign out of %1$d session Sign out of %1$d sessions + Show IP address + Hide IP address Sign out of this session Session details Application, device, and activity information. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index 21cbb86e94..6f002359c8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -29,4 +29,5 @@ sealed class DevicesAction : VectorViewModelAction { object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() object MultiSignoutOtherSessions : DevicesAction() + object ToggleIpAddressVisibility : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index c714645b9a..3cacf82f14 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -116,9 +116,15 @@ class DevicesViewModel @AssistedInject constructor( is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() + DevicesAction.ToggleIpAddressVisibility -> handleToggleIpAddressVisibility() } } + private fun handleToggleIpAddressVisibility() = withState { state -> + val isShowingIpAddress = state.isShowingIpAddress + setState { copy(isShowingIpAddress = !isShowingIpAddress) } + } + private fun handleVerifyCurrentSessionAction() { viewModelScope.launch { val currentSessionCanBeVerified = checkIfCurrentSessionCanBeVerifiedUseCase.execute() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt index e8bed35e24..e0531c34dc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt @@ -27,4 +27,5 @@ data class DevicesViewState( val unverifiedSessionsCount: Int = 0, val inactiveSessionsCount: Int = 0, val isLoading: Boolean = false, + val isShowingIpAddress: Boolean = false, ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 3a3c3463fb..c9957efc58 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -146,11 +146,19 @@ class VectorSettingsDevicesFragment : confirmMultiSignoutOtherSessions() true } + R.id.otherSessionsHeaderToggleIpAddress -> { + handleToggleIpAddressVisibility() + true + } else -> false } } } + private fun handleToggleIpAddressVisibility() { + viewModel.handle(DevicesAction.ToggleIpAddressVisibility) + } + private fun confirmMultiSignoutOtherSessions() { activity?.let { buildConfirmSignoutDialogUseCase.execute(it, this::multiSignoutOtherSessions) @@ -240,7 +248,7 @@ class VectorSettingsDevicesFragment : renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified) renderCurrentDevice(currentDeviceInfo) - renderOtherSessionsView(otherDevices) + renderOtherSessionsView(otherDevices, state.isShowingIpAddress) } else { hideSecurityRecommendations() hideCurrentSessionView() @@ -297,7 +305,7 @@ class VectorSettingsDevicesFragment : hideInactiveSessionsRecommendation() } - private fun renderOtherSessionsView(otherDevices: List?) { + private fun renderOtherSessionsView(otherDevices: List?, isShowingIpAddress: Boolean) { if (otherDevices.isNullOrEmpty()) { hideOtherSessionsView() } else { @@ -313,7 +321,12 @@ class VectorSettingsDevicesFragment : totalNumberOfDevices = otherDevices.size, showViewAll = otherDevices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER ) - } + views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderToggleIpAddress).title = if (isShowingIpAddress) { + stringProvider.getString(R.string.device_manager_other_sessions_hide_ip_address) + } else { + stringProvider.getString(R.string.device_manager_other_sessions_show_ip_address) + } + } } private fun hideOtherSessionsView() { diff --git a/vector/src/main/res/menu/menu_other_sessions_header.xml b/vector/src/main/res/menu/menu_other_sessions_header.xml index 00778ed36e..d1386ba461 100644 --- a/vector/src/main/res/menu/menu_other_sessions_header.xml +++ b/vector/src/main/res/menu/menu_other_sessions_header.xml @@ -4,6 +4,11 @@ xmlns:tools="http://schemas.android.com/tools" tools:ignore="AlwaysShowAction"> + + Date: Mon, 7 Nov 2022 16:52:41 +0100 Subject: [PATCH 132/679] Moving UI auth interceptor into use case --- .../settings/devices/v2/DevicesViewModel.kt | 27 ++----- .../othersessions/OtherSessionsViewModel.kt | 29 ++----- .../v2/overview/SessionOverviewViewModel.kt | 31 +++----- .../InterceptSignoutFlowResponseUseCase.kt | 7 +- .../v2/signout/SignoutSessionUseCase.kt | 42 ---------- ...sult.kt => SignoutSessionsReAuthNeeded.kt} | 16 ++-- .../v2/signout/SignoutSessionsUseCase.kt | 37 ++++++--- .../devices/v2/DevicesViewModelTest.kt | 4 +- .../OtherSessionsViewModelTest.kt | 9 +-- .../overview/SessionOverviewViewModelTest.kt | 12 +-- ...InterceptSignoutFlowResponseUseCaseTest.kt | 12 +-- .../v2/signout/SignoutSessionUseCaseTest.kt | 79 ------------------- .../v2/signout/SignoutSessionsUseCaseTest.kt | 51 +++++++++--- .../app/test/fakes/FakeCryptoService.kt | 22 ++---- .../test/fakes/FakeSignoutSessionUseCase.kt | 77 ------------------ .../test/fakes/FakeSignoutSessionsUseCase.kt | 38 ++------- 16 files changed, 131 insertions(+), 362 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt rename vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/{SignoutSessionResult.kt => SignoutSessionsReAuthNeeded.kt} (71%) delete mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt delete mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index c714645b9a..cd97795b69 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -27,20 +27,16 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, @@ -141,16 +137,14 @@ class DevicesViewModel @AssistedInject constructor( if (deviceIds.isEmpty()) { return@launch } - val signoutResult = signout(deviceIds) + val result = signout(deviceIds) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } @@ -162,16 +156,9 @@ class DevicesViewModel @AssistedInject constructor( .orEmpty() } - private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index c33490400b..9b4c26ee4f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -29,24 +29,18 @@ import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, - private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase ) : VectorSessionsListViewModel( @@ -168,16 +162,14 @@ class OtherSessionsViewModel @AssistedInject constructor( if (deviceIds.isEmpty()) { return@launch } - val signoutResult = signout(deviceIds) + val result = signout(deviceIds) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } @@ -190,16 +182,9 @@ class OtherSessionsViewModel @AssistedInject constructor( }.mapNotNull { it.deviceInfo.deviceId } } - private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 59eeaaadb4..9c4ece7e02 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -30,28 +30,24 @@ import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, - private val signoutSessionUseCase: SignoutSessionUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, @@ -149,30 +145,21 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleSignoutOtherSession(deviceId: String) { viewModelScope.launch { setLoading(true) - val signoutResult = signout(deviceId) + val result = signout(deviceId) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } - private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceId: String) = signoutSessionsUseCase.execute(listOf(deviceId), this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt index 4316995272..42ebd7782e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt @@ -37,17 +37,16 @@ class InterceptSignoutFlowResponseUseCase @Inject constructor( flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation - ): SignoutSessionResult { + ): SignoutSessionsReAuthNeeded? { return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) { UserPasswordAuth( session = null, user = activeSessionHolder.getActiveSession().myUserId, password = reAuthHelper.data ).let { promise.resume(it) } - - SignoutSessionResult.Completed + null } else { - SignoutSessionResult.ReAuthNeeded( + SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = flowResponse.session), uiaContinuation = promise, flowResponse = flowResponse, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt deleted file mode 100644 index bc6cff0d43..0000000000 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.settings.devices.v2.signout - -import im.vector.app.core.di.ActiveSessionHolder -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.util.awaitCallback -import javax.inject.Inject - -/** - * Use case to signout a single session. - */ -class SignoutSessionUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { - - suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { - return deleteDevice(deviceId, userInteractiveAuthInterceptor) - } - - private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { - awaitCallback { matrixCallback -> - activeSessionHolder.getActiveSession() - .cryptoService() - .deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt similarity index 71% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt index fa1fb31b66..56e3d17686 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt @@ -20,13 +20,9 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import kotlin.coroutines.Continuation -sealed class SignoutSessionResult { - data class ReAuthNeeded( - val pendingAuth: UIABaseAuth, - val uiaContinuation: Continuation, - val flowResponse: RegistrationFlowResponse, - val errCode: String? - ) : SignoutSessionResult() - - object Completed : SignoutSessionResult() -} +data class SignoutSessionsReAuthNeeded( + val pendingAuth: UIABaseAuth, + val uiaContinuation: Continuation, + val flowResponse: RegistrationFlowResponse, + val errCode: String? +) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt index b4fc78043e..1cf713a711 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -16,27 +16,42 @@ package im.vector.app.features.settings.devices.v2.signout +import androidx.annotation.Size import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.util.awaitCallback +import timber.log.Timber import javax.inject.Inject +import kotlin.coroutines.Continuation -/** - * Use case to signout several sessions. - */ class SignoutSessionsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, ) { - suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { - return deleteDevices(deviceIds, userInteractiveAuthInterceptor) - } + suspend fun execute( + @Size(min = 1) deviceIds: List, + onReAuthNeeded: (SignoutSessionsReAuthNeeded) -> Unit, + ): Result = runCatching { + Timber.d("start execute with ${deviceIds.size} deviceIds") - private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { - awaitCallback { matrixCallback -> - activeSessionHolder.getActiveSession() - .cryptoService() - .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + val authInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise) + result?.let(onReAuthNeeded) + } } + + deleteDevices(deviceIds, authInterceptor) + Timber.d("end execute") } + + private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 852fc64fd5..65da1a9385 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -228,7 +228,7 @@ class DevicesViewModelTest { // Given val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID) // signout all devices except the current device - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1)) // When val viewModel = createViewModel() @@ -275,7 +275,7 @@ class DevicesViewModelTest { @Test fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { // Given - val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index e01d6e058c..1e8c511c42 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -23,7 +23,6 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase @@ -66,7 +65,6 @@ class OtherSessionsViewModelTest { private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() - private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = @@ -75,7 +73,6 @@ class OtherSessionsViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, - interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) @@ -321,7 +318,7 @@ class OtherSessionsViewModelTest { val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout only selected devices - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2)) val expectedViewState = OtherSessionsViewState( devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), currentFilter = defaultArgs.defaultFilter, @@ -357,7 +354,7 @@ class OtherSessionsViewModelTest { val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout all devices - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedViewState = OtherSessionsViewState( devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), currentFilter = defaultArgs.defaultFilter, @@ -422,7 +419,7 @@ class OtherSessionsViewModelTest { val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) - val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index b2ab939bd1..f26c818e1d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -28,7 +28,7 @@ import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowRe import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler -import im.vector.app.test.fakes.FakeSignoutSessionUseCase +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -70,7 +70,7 @@ class SessionOverviewViewModelTest { private val getDeviceFullInfoUseCase = mockk(relaxed = true) private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) @@ -82,7 +82,7 @@ class SessionOverviewViewModelTest { initialState = SessionOverviewViewState(args), getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionUseCase = fakeSignoutSessionUseCase.instance, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, @@ -248,7 +248,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - fakeSignoutSessionUseCase.givenSignoutSuccess(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_SESSION_ID_1)) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -285,7 +285,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val error = Exception() - fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, error) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_SESSION_ID_1), error) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -318,7 +318,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val reAuthNeeded = fakeSignoutSessionUseCase.givenSignoutReAuthNeeded(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_SESSION_ID_1)) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt index 35551ba36e..cd0575f2a0 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt @@ -24,8 +24,8 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs import io.mockk.unmockkAll +import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeInstanceOf import org.junit.After import org.junit.Before import org.junit.Test @@ -63,7 +63,7 @@ class InterceptSignoutFlowResponseUseCaseTest { } @Test - fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and success is returned`() { + fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and null is returned`() { // Given val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID) fakeReAuthHelper.givenStoredPassword(A_PASSWORD) @@ -84,7 +84,7 @@ class InterceptSignoutFlowResponseUseCaseTest { ) // Then - result shouldBeInstanceOf (SignoutSessionResult.Completed::class) + result shouldBe null every { promise.resume(expectedAuth) } @@ -97,7 +97,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(A_PASSWORD) val errorCode = AN_ERROR_CODE val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, @@ -122,7 +122,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(A_PASSWORD) val errorCode: String? = null val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, @@ -147,7 +147,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(null) val errorCode: String? = null val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt deleted file mode 100644 index 5af91c16ce..0000000000 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.settings.devices.v2.signout - -import im.vector.app.test.fakes.FakeActiveSessionHolder -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBe -import org.junit.Test -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor - -private const val A_DEVICE_ID = "device-id" - -class SignoutSessionUseCaseTest { - - private val fakeActiveSessionHolder = FakeActiveSessionHolder() - - private val signoutSessionUseCase = SignoutSessionUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance - ) - - @Test - fun `given a device id when signing out with success then success result is returned`() = runTest { - // Given - val interceptor = givenAuthInterceptor() - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .givenDeleteDeviceSucceeds(A_DEVICE_ID) - - // When - val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor) - - // Then - result.isSuccess shouldBe true - every { - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .deleteDevice(A_DEVICE_ID, interceptor, any()) - } - } - - @Test - fun `given a device id when signing out with error then failure result is returned`() = runTest { - // Given - val interceptor = givenAuthInterceptor() - val error = mockk() - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .givenDeleteDeviceFailsWithError(A_DEVICE_ID, error) - - // When - val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor) - - // Then - result.isFailure shouldBe true - every { - fakeActiveSessionHolder.fakeSession - .fakeCryptoService - .deleteDevice(A_DEVICE_ID, interceptor, any()) - } - } - - private fun givenAuthInterceptor() = mockk() -} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt index 08a9fa625b..70d2b4b039 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -19,10 +19,10 @@ package im.vector.app.features.settings.devices.v2.signout import im.vector.app.test.fakes.FakeActiveSessionHolder import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.junit.Test -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" @@ -30,36 +30,38 @@ private const val A_DEVICE_ID_2 = "device-id-2" class SignoutSessionsUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val signoutSessionsUseCase = SignoutSessionsUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance + activeSessionHolder = fakeActiveSessionHolder.instance, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, ) @Test fun `given a list of device ids when signing out with success then success result is returned`() = runTest { // Given - val interceptor = givenAuthInterceptor() + val callback = givenOnReAuthCallback() val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) fakeActiveSessionHolder.fakeSession .fakeCryptoService .givenDeleteDevicesSucceeds(deviceIds) // When - val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + val result = signoutSessionsUseCase.execute(deviceIds, callback) // Then result.isSuccess shouldBe true - every { + verify { fakeActiveSessionHolder.fakeSession .fakeCryptoService - .deleteDevices(deviceIds, interceptor, any()) + .deleteDevices(deviceIds, any(), any()) } } @Test fun `given a list of device ids when signing out with error then failure result is returned`() = runTest { // Given - val interceptor = givenAuthInterceptor() + val interceptor = givenOnReAuthCallback() val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) val error = mockk() fakeActiveSessionHolder.fakeSession @@ -71,12 +73,41 @@ class SignoutSessionsUseCaseTest { // Then result.isFailure shouldBe true - every { + verify { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, any(), any()) + } + } + + @Test + fun `given a list of device ids when signing out with reAuth needed then callback is called`() = runTest { + // Given + val callback = givenOnReAuthCallback() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesNeedsUIAuth(deviceIds) + val reAuthNeeded = SignoutSessionsReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = mockk(), + flowResponse = mockk(), + errCode = "errorCode" + ) + every { fakeInterceptSignoutFlowResponseUseCase.execute(any(), any(), any()) } returns reAuthNeeded + + // When + val result = signoutSessionsUseCase.execute(deviceIds, callback) + + // Then + result.isSuccess shouldBe true + verify { fakeActiveSessionHolder.fakeSession .fakeCryptoService - .deleteDevices(deviceIds, interceptor, any()) + .deleteDevices(deviceIds, any(), any()) + callback(reAuthNeeded) } } - private fun givenAuthInterceptor() = mockk() + private fun givenOnReAuthCallback(): (SignoutSessionsReAuthNeeded) -> Unit = {} } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index 5f34c45fa7..b23f018cf5 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -22,6 +22,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.slot import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo @@ -70,30 +71,21 @@ class FakeCryptoService( } } - fun givenDeleteDeviceSucceeds(deviceId: String) { - val matrixCallback = slot>() - every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { + fun givenDeleteDevicesSucceeds(deviceIds: List) { + every { deleteDevices(deviceIds, any(), any()) } answers { thirdArg>().onSuccess(Unit) } } - fun givenDeleteDeviceFailsWithError(deviceId: String, error: Exception) { - val matrixCallback = slot>() - every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { - thirdArg>().onFailure(error) - } - } - - fun givenDeleteDevicesSucceeds(deviceIds: List) { - val matrixCallback = slot>() - every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + fun givenDeleteDevicesNeedsUIAuth(deviceIds: List) { + every { deleteDevices(deviceIds, any(), any()) } answers { + secondArg().performStage(mockk(), "", mockk()) thirdArg>().onSuccess(Unit) } } fun givenDeleteDevicesFailsWithError(deviceIds: List, error: Exception) { - val matrixCallback = slot>() - every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + every { deleteDevices(deviceIds, any(), any()) } answers { thirdArg>().onFailure(error) } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt deleted file mode 100644 index 8a6b101ff6..0000000000 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.test.fakes - -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import kotlin.coroutines.Continuation - -class FakeSignoutSessionUseCase { - - val instance = mockk() - - fun givenSignoutSuccess( - deviceId: String, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - } - - fun givenSignoutReAuthNeeded( - deviceId: String, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() - val flowResponse = mockk() - every { flowResponse.session } returns "a-session-id" - val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( - pendingAuth = mockk(), - uiaContinuation = promise, - flowResponse = flowResponse, - errCode = errorCode, - ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - - return reAuthNeeded - } - - fun givenSignoutError(deviceId: String, error: Throwable) { - coEvery { instance.execute(deviceId, any()) } returns Result.failure(error) - } -} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt index 04d05b1d8a..9eb3676475 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt @@ -16,55 +16,33 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.mockk.slot -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import kotlin.coroutines.Continuation class FakeSignoutSessionsUseCase { val instance = mockk() - fun givenSignoutSuccess( - deviceIds: List, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } + fun givenSignoutSuccess(deviceIds: List) { + coEvery { instance.execute(deviceIds, any()) } returns Result.success(Unit) } - fun givenSignoutReAuthNeeded( - deviceIds: List, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() + fun givenSignoutReAuthNeeded(deviceIds: List): SignoutSessionsReAuthNeeded { val flowResponse = mockk() every { flowResponse.session } returns "a-session-id" val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + val reAuthNeeded = SignoutSessionsReAuthNeeded( pendingAuth = mockk(), - uiaContinuation = promise, + uiaContinuation = mockk(), flowResponse = flowResponse, errCode = errorCode, ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) + coEvery { instance.execute(deviceIds, any()) } coAnswers { + secondArg<(SignoutSessionsReAuthNeeded) -> Unit>().invoke(reAuthNeeded) Result.success(Unit) } From 3a5af934ccc68195a7041d45f2346762d60d6535 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 23:15:05 +0000 Subject: [PATCH 133/679] Bump play-services-location from 21.0.0 to 21.0.1 Bumps play-services-location from 21.0.0 to 21.0.1. --- updated-dependencies: - dependency-name: com.google.android.gms:play-services-location dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index f793fff2c8..5b8d5b5331 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -372,7 +372,7 @@ dependencies { debugImplementation 'com.facebook.soloader:soloader:0.10.4' debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" - gplayImplementation "com.google.android.gms:play-services-location:21.0.0" + gplayImplementation "com.google.android.gms:play-services-location:21.0.1" // UnifiedPush gplay flavor only gplayImplementation('com.google.firebase:firebase-messaging:23.1.0') { exclude group: 'com.google.firebase', module: 'firebase-core' From 49bf0e18fc33d597f1cfbf360543522eabbf530a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 23:20:06 +0000 Subject: [PATCH 134/679] Bump sentry-android from 6.6.0 to 6.7.0 Bumps [sentry-android](https://github.com/getsentry/sentry-java) from 6.6.0 to 6.7.0. - [Release notes](https://github.com/getsentry/sentry-java/releases) - [Changelog](https://github.com/getsentry/sentry-java/blob/6.7.0/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-java/compare/6.6.0...6.7.0) --- updated-dependencies: - dependency-name: io.sentry:sentry-android dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 47f1097446..751cba4d44 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -26,7 +26,7 @@ def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.6.0" +def sentry = "6.7.0" def fragment = "1.5.4" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 From e84e2a10fd3d5370923c8a50f55a1684957c7c6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 23:20:45 +0000 Subject: [PATCH 135/679] Bump libphonenumber from 8.12.57 to 8.13.0 Bumps [libphonenumber](https://github.com/google/libphonenumber) from 8.12.57 to 8.13.0. - [Release notes](https://github.com/google/libphonenumber/releases) - [Changelog](https://github.com/google/libphonenumber/blob/master/making-metadata-changes.md) - [Commits](https://github.com/google/libphonenumber/compare/v8.12.57...v8.13.0) --- updated-dependencies: - dependency-name: com.googlecode.libphonenumber:libphonenumber dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 47f1097446..101b769576 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -83,7 +83,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.57" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.0" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", From 001ab8cb4958a8a247dd840f97f6987c8dc0a02a Mon Sep 17 00:00:00 2001 From: "Auri B. P" Date: Sat, 5 Nov 2022 21:56:13 +0000 Subject: [PATCH 136/679] Translated using Weblate (Catalan) Currently translated at 99.7% (2532 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ca/ --- library/ui-strings/src/main/res/values-ca/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml index ce786fb87d..f9d7145b66 100644 --- a/library/ui-strings/src/main/res/values-ca/strings.xml +++ b/library/ui-strings/src/main/res/values-ca/strings.xml @@ -2836,4 +2836,5 @@ Adjunts Adhesius Galeria + Format de text \ No newline at end of file From 2652b7ce636bbc21ff066aca4c9f048ac1fe53b6 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 4 Nov 2022 14:49:46 +0000 Subject: [PATCH 137/679] Translated using Weblate (Czech) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- library/ui-strings/src/main/res/values-cs/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index 53599adce2..47caa52149 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2891,4 +2891,12 @@ %1$d vybrané %1$d vybraných + Přepnutí režimu celé obrazovky + Formátování textu + Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a zahajte nové. + Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a zahajte nové. + Nemáte potřebná oprávnění k zahájení hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění. + Nelze zahájit nové hlasové vysílání + Přetočení o 30 sekund zpět + Přetočení o 30 sekund dopředu \ No newline at end of file From dac544577285ee891d0649b4064edbd227588dc1 Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 4 Nov 2022 15:26:12 +0000 Subject: [PATCH 138/679] Translated using Weblate (German) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- library/ui-strings/src/main/res/values-de/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 409fc564f4..cd215e175d 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2835,4 +2835,12 @@ %1$d ausgewählt %1$d ausgewählt + Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen. + Sprachübertragung kann nicht gestartet werden + Vollbildmodus umschalten + Textformatierung + Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen. + Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest. + 30 Sekunden vorspulen + 30 Sekunden zurückspulen \ No newline at end of file From 6e46cdded69525b8b9a844095e7552fd09e67f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 6 Nov 2022 07:18:57 +0000 Subject: [PATCH 139/679] Translated using Weblate (Estonian) Currently translated at 99.6% (2531 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- library/ui-strings/src/main/res/values-et/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 9bfbbe8eeb..22572a0f36 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2827,4 +2827,12 @@ %1$d valitud %1$d valitud + Lülita täisekraanivaade sisse/välja + Tekstivorming + Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus. + Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud. + Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga. + Uue ringhäälingukõne alustamine pole võimalik + Keri tagasi 30 sekundi kaupa + Keri edasi 30 sekundi kaupa \ No newline at end of file From 546f391c577c27b53a2ee7dc45547693ac62b641 Mon Sep 17 00:00:00 2001 From: Glandos Date: Sat, 5 Nov 2022 13:36:12 +0000 Subject: [PATCH 140/679] Translated using Weblate (French) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- library/ui-strings/src/main/res/values-fr/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index 6c767fc350..a02b062596 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2836,4 +2836,12 @@ %1$d sélectionné %1$d sélectionnés + Basculer en mode plein écran + Formatage de texte + Vous êtes déjà en train de réaliser une diffusion audio. Veuillez terminer votre diffusion audio actuelle pour en démarrer une nouvelle. + Une autre personne est déjà en train de réaliser une diffusion audio. Attendez que sa diffusion audio soit terminée pour en démarrer une nouvelle. + Vous n’avez pas les permissions requises pour démarrer une nouvelle diffusion audio dans ce salon. Contactez un administrateur du salon pour mettre-à-jour vos permissions. + Impossible de commencer une nouvelle diffusion audio + Avance rapide de 30 secondes + Retour rapide de 30 secondes \ No newline at end of file From 4a00be4e8a2f5ec55eeb440b02bca43562789f00 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 5 Nov 2022 10:26:39 +0000 Subject: [PATCH 141/679] Translated using Weblate (Indonesian) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/id/ --- library/ui-strings/src/main/res/values-in/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index 6ed423bb70..cde367faf9 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2783,4 +2783,12 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. %1$d dipilih + Ubah mode layar penuh + Format teks + Anda sedang merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru. + Orang lain sedang merekam sebuah siaran suara. Tunggu untuk siaran suara berakhir untuk memulai yang baru. + Anda tidak memiliki izin yang dibutuhkan untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda. + Tidak dapat memulai siaran suara baru + Maju cepat 30 detik + Mundur cepat 30 detik \ No newline at end of file From fd70e648c11b6e5b4c1c5a47ecbc180120de8b7d Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Fri, 4 Nov 2022 21:31:37 +0000 Subject: [PATCH 142/679] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/ --- library/ui-strings/src/main/res/values-pt-rBR/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index 2ec5f394bd..d3061371fa 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -2836,4 +2836,12 @@ %1$d selecionada(o) %1$d selecionadas(os) + Alguma outra pessoa já está gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo. + Alternar modo de tela cheia + Formatação de texto + Você já está gravando um broadcast de voz. Por favor termine seu broadcast de voz atual para começar um novo. + Você não tem as permissões requeridas para começar um broadcast de voz nesta sala. Contacte um/uma administrador(a) para fazer upgrade de suas permissões. + Não dá pra começar um novo broadcast de voz + Avançar rápido 30 segundos + Retroceder 30 segundos \ No newline at end of file From 89bb2d1a9bfde0b9215055416cf52aafa3ee8bfe Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 4 Nov 2022 17:21:58 +0000 Subject: [PATCH 143/679] Translated using Weblate (Slovak) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index a41aca05dc..9eac092a62 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2891,4 +2891,12 @@ %1$d vybraté %1$d vybraných + Prepnutie režimu na celú obrazovku + Formátovanie textu + Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové. + Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové. + Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia. + Nie je možné spustiť nové hlasové vysielanie + Rýchle posunutie dozadu o 30 sekúnd + Rýchle posunutie dopredu o 30 sekúnd \ No newline at end of file From 5d5ea81db897a816d5e3a2fa19e3f225c40c13c9 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sun, 6 Nov 2022 15:48:31 +0000 Subject: [PATCH 144/679] Translated using Weblate (Albanian) Currently translated at 99.0% (2516 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/ --- .../src/main/res/values-sq/strings.xml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index b1d8eb9564..773454c39f 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -896,7 +896,7 @@ Shfaq te rrjedha kohore akte të fshehura Përgjegjës integrimesh ID Aplikacioni: - + Emër Aplikacioni Në Ekran: Emër Sesioni Në Ekran: Mesazhe të Drejtpërdrejtë @@ -1275,7 +1275,7 @@ \n - Shërbyesi Home te i cili është lidhur përdoruesi që po verifikoni \n - Lidhja juaj internet ose ajo e përdoruesit tjetër \n - Pajisja juaj ose ajo e përdoruesit tjetër - + %s u anulua %s u pranua Skanojeni kodin me pajisjen e përdoruesit tjetër, për të verifikuar në mënyrë të sigurt njëri-tjetrin Nëse s’jeni vetë atje, krahasoni emoji-n @@ -2808,4 +2808,18 @@ %1$d i përzgjedhura %1$d të përzgjedhura + Shërbyesi Home nuk mbulon hyrje me kod QR. + U has një problem sigurie, kur ujdisej shkëmbim i siguruar mesazhesh. Mund të jetë komprometuar një nga sa vijon: shërbyesi juaj Home; lidhja(et) tuaja internet; pajisja(et) tuaja; + Lidhja s’u plotësua në kohën e duhur. + Kontrolloni pajisjen ku jeni i futur, duhet të shfaqet kodi më poshtë. Sigurohuni se kodi më poshtë përputhet me atë pajisje: + Skanoni kodin QR më poshtë me pajisjen tuaj prej nga është dalë nga llogaria. + Përdorni pajisjen tuaj ku jeni brenda llogarisë që të skanoni kodin QR më poshtë: + Përdorni kamerën në këtë pajisje që të skanoni kodin QR të shfaqur në pajisjen tuaj tjetër: + Mirato vetvetiu widget-e Thirrjesh Element Call dhe akordo përdorim kamere / mikfrofoni + MSC3061: Po jepen kyçe dhome për mesazhe të dikurshëm + Shfaq hollësitë më të reja të profileve (avatar dhe emër në ekran) për krejt mesazhet. + Kërko doemos që tastiera të mos përditësojë ndonjë të dhënë të personalizuar, bie fjala, historik shtypjeje në të dhe fjalor bazuar në ç’keni shtypur në biseda. Kini parasysh se disa tastiera mund të mos e respektojnë këtë rregullim. + Ky kod QR duket i formuar keq. Ju lutemi, provoni ta verifikoni me tjetër metodë. + 🔒 Keni aktivizuar fshehtëzim për sesionie të verifikuar vetëm për krejt dhomat, që nga Rregullime Sigurie. + Luaj figura të animuara te rrjedha kohora sapo zënë të duken \ No newline at end of file From 0904f9c6e0b1d54722af6d163b7a52f1632241f6 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 4 Nov 2022 19:04:33 +0000 Subject: [PATCH 145/679] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- library/ui-strings/src/main/res/values-uk/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index f633a0ef2f..8cbfeca6ba 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2946,4 +2946,12 @@ Вибрано %1$d Вибрати все + Перемкнути повноекранний режим + Форматування тексту + Ви вже записуєте голосове повідомлення. Завершіть поточну трансляцію, щоб розпочати нову. + Хтось інший вже записує голосове повідомлення. Зачекайте, поки закінчиться трансляція, щоб розпочати нову. + Ви не маєте необхідних дозволів для початку передавання голосового повідомлення в цю кімнату. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи. + Не вдалося розпочати передавання нового голосового повідомлення + Перемотати вперед на 30 секунд + Перемотати назад на 30 секунд \ No newline at end of file From 6161a9582e03cfe0ff4e1773866fefd0fde84dbc Mon Sep 17 00:00:00 2001 From: PotLice Date: Mon, 7 Nov 2022 09:06:07 +0000 Subject: [PATCH 146/679] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hans/ --- .../src/main/res/values-zh-rCN/strings.xml | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 112b900da7..5ab8a351d1 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -348,7 +348,7 @@ 显示系统设置中的应用程序信息。 通话请求 使用条款 - 其他 + 其它 通知目标 登录为 请检查你的电子邮件并点击里面包含的链接。完成时请点击继续。 @@ -434,7 +434,7 @@ 你添加了一个新会话“%s”,它正在请求加密密钥。 你的未验证会话“%s”正在请求加密密钥。 开始验证 - bug报告 + 错误报告 拍摄照片 拍摄视频 使用原生相机 @@ -1714,7 +1714,7 @@ 建议 已知用户 二维码 - 通过QR码添加 + 通过二维码添加 房间设置 话题 房间话题(可选) @@ -2099,7 +2099,7 @@ 我的用户名 我的显示名称 通知事项 - 其他 + 其它 提及和关键词 默认通知 可用视频通话 @@ -2303,7 +2303,7 @@ BETA 共享你的实时位置 缩放到当前位置 - 地图上选定位置的图钉 + 地图上选定位置的固定标记 无投票 验证你的电子邮件 @@ -2336,12 +2336,12 @@ 共享位置 您需要拥有正确的权限才能在此房间中共享实时位置。 你没有权限共享实时位置 - %1$s前已更新 + %1$s 前已更新 临时执行:地点在房间历史中持续存在 启用实时位置共享 位置共享正在进行中 - ${app_name}实时位置 - 剩余%1$s + ${app_name} 实时位置 + 剩余 %1$s 停止 实时共享直到 %1$s 查看实时位置 @@ -2350,14 +2350,14 @@ 启用实时位置 加载地图失败 打开,用 - ${app_name}无法访问你的位置。请稍后再试。 - ${app_name}无法访问你的位置 + ${app_name} 无法访问你的位置。请稍后再试。 + ${app_name} 无法访问你的位置 在房间中查看 MSC3061:为过去的消息共享房间密钥 在共享历史的加密房间中邀请时,加密历史将可见。 - 8小时 - 1小时 - 15分钟 + 8 小时 + 1 小时 + 15 分钟 共享此位置 共享此位置 共享实时位置 @@ -2539,7 +2539,7 @@ 自动允许 Element 通话小部件并授予相机/麦克风访问权限 启用 Element 通话权限快捷方式 实时位置 - 这个QR码看起来不正常。请尝试用另一个方法验证。 + 此二维码看起来格式不正确。请尝试使用其它方法进行验证。 你无法访问加密消息历史。重置你的安全消息备份和验证密钥以重新开始。 无法验证此设备 你的服务器地址是什么? @@ -2562,7 +2562,7 @@ A—Z 活动 排序方式 - 显示最近的 + 显示最近 显示过滤条件 布局偏好 探索房间 @@ -2622,7 +2622,7 @@ 你当前的会话已准备好安全地收发消息。 仅在首条消息创建私聊消息 启用延迟的私聊消息 - 简化的Element,带有可选的标签 + 简化的 Element,带有可选的标签 无痕键盘 请求键盘不要根据您在对话中输入的内容更新任何个性化数据,例如输入历史记录和字典。 请注意,某些键盘可能不遵守此设置。 ${app_name}需要权限来显示通知。通知可以显示消息、邀请等。 @@ -2762,9 +2762,31 @@ 停止语音广播录制 暂停语音广播录制 继续语音广播录制 - 扫描QR码 + 扫描二维码 语音广播 已启用: 会话ID: 出了点差错。请检查您的网络连接并重试。 + 联系人 + 切换全屏模式 + 选择会话 + 文本格式 + 相机 + 位置 + 投票 + 语音广播 + 附件 + 贴纸 + 照片库 + 您没有在此房间内开始语音广播所需的权限。联系房间管理员升级您的权限。 + 其他人已经在录制语音广播。等待他们的语音广播结束以开始新的广播。 + 您已经在录制语音广播。请结束您当前的语音广播以开始新的语音广播。 + 无法开始新的语音广播 + 快进 30 秒 + 快退 30 秒 + 取消全选 + 全选 + + 已选择 %1$d + \ No newline at end of file From 191034a7d384e4d06d035ab015de840feb64f9f9 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 7 Nov 2022 02:33:01 +0000 Subject: [PATCH 147/679] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2539 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/ --- library/ui-strings/src/main/res/values-zh-rTW/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index 739ea09755..91e08c803a 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2781,4 +2781,12 @@ 已選取 %1$d + 切換全螢幕模式 + 文字格式化 + 您已在錄製語音廣播。請結束您目前的語音廣播以開始新的。 + 其他人已在錄製語音廣播。等待他們的語音廣播結束以開始新的。 + 您沒有在此聊天室中開始語音廣播的必要權限。請聯絡聊天室管理員以升級您的權限。 + 無法開始新的語音廣播 + 快轉30秒 + 快退30秒 \ No newline at end of file From 718545cd4d5da4e7a3ab18edcfc2e942e2d171f3 Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 4 Nov 2022 15:19:12 +0000 Subject: [PATCH 148/679] Translated using Weblate (German) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/de/ --- fastlane/metadata/android/de-DE/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/40105060.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/40105060.txt b/fastlane/metadata/android/de-DE/changelogs/40105060.txt new file mode 100644 index 0000000000..0b36faff1e --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Die wichtigste Änderung in dieser Version: Neues Anhangauswahl-UI. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases From ab1c720a19eb58df3bc6264fc3b0fe40fccf172a Mon Sep 17 00:00:00 2001 From: Glandos Date: Sat, 5 Nov 2022 13:34:45 +0000 Subject: [PATCH 149/679] Translated using Weblate (French) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/ --- fastlane/metadata/android/fr-FR/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40105060.txt diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105060.txt b/fastlane/metadata/android/fr-FR/changelogs/40105060.txt new file mode 100644 index 0000000000..b33f290d0d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : nouvelle interface de sélection d’une pièce jointe. +Intégralité des changements : https://github.com/vector-im/element-android/releases From 89554b7f9ece1610f7d0e89373b0aa45e4d188b9 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Fri, 4 Nov 2022 21:32:44 +0000 Subject: [PATCH 150/679] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/ --- fastlane/metadata/android/pt-BR/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40105060.txt diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105060.txt b/fastlane/metadata/android/pt-BR/changelogs/40105060.txt new file mode 100644 index 0000000000..108a8a88b4 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: novo UI para selecionar um anexo. +Changelog completo: https://github.com/vector-im/element-android/releases From 0d002dbd26e13187d87ff0ce93dfe0518bf8defc Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 4 Nov 2022 17:19:19 +0000 Subject: [PATCH 151/679] Translated using Weblate (Slovak) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/ --- fastlane/metadata/android/sk/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sk/changelogs/40105060.txt diff --git a/fastlane/metadata/android/sk/changelogs/40105060.txt b/fastlane/metadata/android/sk/changelogs/40105060.txt new file mode 100644 index 0000000000..0d1d4965ca --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: nové používateľské rozhranie na výber príloh. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases From cfbbfc6cb5f1d8c002bd084995f831e5fb165314 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 4 Nov 2022 19:19:22 +0000 Subject: [PATCH 152/679] Translated using Weblate (Ukrainian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/ --- fastlane/metadata/android/uk/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/40105060.txt diff --git a/fastlane/metadata/android/uk/changelogs/40105060.txt b/fastlane/metadata/android/uk/changelogs/40105060.txt new file mode 100644 index 0000000000..4be635901f --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: новий інтерфейс для вибору вкладення. +Перелік усіх змін: https://github.com/vector-im/element-android/releases From 54259f2f40eb8b71e3e1e052d384894d8645a35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 6 Nov 2022 07:16:40 +0000 Subject: [PATCH 153/679] Translated using Weblate (Estonian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/et/ --- fastlane/metadata/android/et/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/40105060.txt diff --git a/fastlane/metadata/android/et/changelogs/40105060.txt b/fastlane/metadata/android/et/changelogs/40105060.txt new file mode 100644 index 0000000000..d5606e24b3 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: uus liides manuste lisamiseks. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases From 9bb2157477a4213f9554bf315d58e727ae0c7a7d Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Mon, 7 Nov 2022 02:30:36 +0000 Subject: [PATCH 154/679] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/ --- fastlane/metadata/android/zh-TW/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-TW/changelogs/40105060.txt diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105060.txt b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt new file mode 100644 index 0000000000..56667ccfc0 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105060.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:選取附件的新使用者介面。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases From 1d3e61aa53f2fc4dfb43d3583d158850ed4afafc Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 4 Nov 2022 14:46:17 +0000 Subject: [PATCH 155/679] Translated using Weblate (Czech) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/ --- fastlane/metadata/android/cs-CZ/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/40105060.txt diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt new file mode 100644 index 0000000000..e966dbbd92 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: nové uživatelské rozhraní pro výběr přílohy. +Úplný seznam změn: https://github.com/vector-im/element-android/releases From 8dbd170b760a1a48aaa4036d53788e73eb230c31 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 5 Nov 2022 10:22:29 +0000 Subject: [PATCH 156/679] Translated using Weblate (Indonesian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/id/ --- fastlane/metadata/android/id/changelogs/40105060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/id/changelogs/40105060.txt diff --git a/fastlane/metadata/android/id/changelogs/40105060.txt b/fastlane/metadata/android/id/changelogs/40105060.txt new file mode 100644 index 0000000000..32fb87563e --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Antarmuka baru untuk memilih sebuah lampiran. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases From 2796f1b0be0d6870561298b7dd4175a10db0ea30 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sun, 6 Nov 2022 16:36:25 +0000 Subject: [PATCH 157/679] Translated using Weblate (Albanian) Currently translated at 100.0% (80 of 80 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/ --- fastlane/metadata/android/sq/changelogs/40104120.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104130.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104140.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104160.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104180.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104190.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104200.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104220.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104230.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104240.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104250.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104260.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104270.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104280.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104300.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104310.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104320.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104340.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40104360.txt | 3 +++ fastlane/metadata/android/sq/changelogs/40105000.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40105020.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40105040.txt | 2 ++ fastlane/metadata/android/sq/changelogs/40105060.txt | 2 ++ 23 files changed, 47 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/40104120.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104130.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104140.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104160.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104180.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104190.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104200.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104220.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104230.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104240.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104250.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104260.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104270.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104280.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104300.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104310.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104320.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104340.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40104360.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105000.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105020.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105040.txt create mode 100644 fastlane/metadata/android/sq/changelogs/40105060.txt diff --git a/fastlane/metadata/android/sq/changelogs/40104120.txt b/fastlane/metadata/android/sq/changelogs/40104120.txt new file mode 100644 index 0000000000..f93220235b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: U lejon përdoruesve të shfaqen si jo në linjë dhe shton një lojtës audio për bashkëngjitje audio +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104130.txt b/fastlane/metadata/android/sq/changelogs/40104130.txt new file mode 100644 index 0000000000..f93220235b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104130.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: U lejon përdoruesve të shfaqen si jo në linjë dhe shton një lojtës audio për bashkëngjitje audio +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104140.txt b/fastlane/metadata/android/sq/changelogs/40104140.txt new file mode 100644 index 0000000000..c8b2eb09ab --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104140.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përmirësim i administrimit të përdoruesve të shpërfillur. Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104160.txt b/fastlane/metadata/android/sq/changelogs/40104160.txt new file mode 100644 index 0000000000..987197f0f6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104160.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Administrim më i mirë i mesazheve të fshehtëzuar. Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104180.txt b/fastlane/metadata/android/sq/changelogs/40104180.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104180.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104190.txt b/fastlane/metadata/android/sq/changelogs/40104190.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104190.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104200.txt b/fastlane/metadata/android/sq/changelogs/40104200.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104200.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104220.txt b/fastlane/metadata/android/sq/changelogs/40104220.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104220.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104230.txt b/fastlane/metadata/android/sq/changelogs/40104230.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104230.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104240.txt b/fastlane/metadata/android/sq/changelogs/40104240.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104240.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104250.txt b/fastlane/metadata/android/sq/changelogs/40104250.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104250.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104260.txt b/fastlane/metadata/android/sq/changelogs/40104260.txt new file mode 100644 index 0000000000..c5ffad38c9 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Përdorim i UnifiedPush dhe lejim i përdoruesve të kenë push pa FCM. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104270.txt b/fastlane/metadata/android/sq/changelogs/40104270.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104280.txt b/fastlane/metadata/android/sq/changelogs/40104280.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104280.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104300.txt b/fastlane/metadata/android/sq/changelogs/40104300.txt new file mode 100644 index 0000000000..6c1be8f556 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104300.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Bërje e mundur hapash të përmirësuar hyrje dhe dalje nga llogaria. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104310.txt b/fastlane/metadata/android/sq/changelogs/40104310.txt new file mode 100644 index 0000000000..6c1be8f556 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104310.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Bërje e mundur hapash të përmirësuar hyrje dhe dalje nga llogaria. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104320.txt b/fastlane/metadata/android/sq/changelogs/40104320.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104320.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104340.txt b/fastlane/metadata/android/sq/changelogs/40104340.txt new file mode 100644 index 0000000000..87f801d1f4 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104340.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash dhe përmirësime të ndryshme qëndrueshmërie. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40104360.txt b/fastlane/metadata/android/sq/changelogs/40104360.txt new file mode 100644 index 0000000000..ef9251a497 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40104360.txt @@ -0,0 +1,3 @@ +Skema e re e Aplikacionit mund të aktivizohet që nga rregullimet Labs. Ju lutemi, provojeni! +Ndreqje problemesh me njoftim që mungon dhe njëkohësim i gjatë shtues. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105000.txt b/fastlane/metadata/android/sq/changelogs/40105000.txt new file mode 100644 index 0000000000..2ee2ded823 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105000.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Hedhje poshtë MD e aktivizuar, si parazgjedhje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105020.txt b/fastlane/metadata/android/sq/changelogs/40105020.txt new file mode 100644 index 0000000000..26647d519f --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105020.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Skema e re e aplikacionit e aktivizuar, si parazgjedhje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105040.txt b/fastlane/metadata/android/sq/changelogs/40105040.txt new file mode 100644 index 0000000000..4e38434f89 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105040.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Veçori të reja nën rregullimet Labs: hartues teksti të pasur, administrim i ri pajisjesh, transmetim zanor. Ende nën zhvillim aktivt! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sq/changelogs/40105060.txt b/fastlane/metadata/android/sq/changelogs/40105060.txt new file mode 100644 index 0000000000..eb300bafed --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: ndërfaqe e re UI për përzgjedhjen e një bashkëngjitjeje! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases From eed2a74d07ec82558134f2eb36377c94d9458f43 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 8 Nov 2022 14:36:27 +0300 Subject: [PATCH 158/679] Toggle ip address on others section of the main screen. --- .../devices/v2/VectorSettingsDevicesFragment.kt | 7 ++++--- .../settings/devices/v2/list/OtherSessionItem.kt | 6 ++++++ .../devices/v2/list/OtherSessionsController.kt | 1 + vector/src/main/res/layout/item_other_session.xml | 15 +++++++++++++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index c9957efc58..b27d8a7270 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -316,10 +316,11 @@ class VectorSettingsDevicesFragment : multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) multiSignoutItem.setTextColor(color) views.deviceListOtherSessions.isVisible = true + val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) } views.deviceListOtherSessions.render( - devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), - totalNumberOfDevices = otherDevices.size, - showViewAll = otherDevices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER + devices = devices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), + totalNumberOfDevices = devices.size, + showViewAll = devices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER ) views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderToggleIpAddress).title = if (isShowingIpAddress) { stringProvider.getString(R.string.device_manager_other_sessions_hide_ip_address) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt index de1cd33d35..9d9cb15c28 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt @@ -29,6 +29,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider @@ -69,6 +70,9 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la @EpoxyAttribute var selected: Boolean = false + @EpoxyAttribute + var ipAddress: String? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null @@ -100,6 +104,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la holder.otherSessionDescriptionTextView.setTextColor(it) } holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null) + holder.otherSessionIpAddressTextView.setTextOrHide(ipAddress) holder.otherSessionItemBackgroundView.isSelected = selected } @@ -108,6 +113,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la val otherSessionVerificationStatusImageView by bind(R.id.otherSessionVerificationStatusImageView) val otherSessionNameTextView by bind(R.id.otherSessionNameTextView) val otherSessionDescriptionTextView by bind(R.id.otherSessionDescriptionTextView) + val otherSessionIpAddressTextView by bind(R.id.otherSessionIpAddressTextView) val otherSessionItemBackgroundView by bind(R.id.otherSessionItemBackground) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt index 8d70552101..5e2549f42a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt @@ -72,6 +72,7 @@ class OtherSessionsController @Inject constructor( sessionDescription(description) sessionDescriptionDrawable(descriptionDrawable) sessionDescriptionColor(descriptionColor) + ipAddress(device.deviceInfo.lastSeenIp) stringProvider(host.stringProvider) colorProvider(host.colorProvider) drawableProvider(host.drawableProvider) diff --git a/vector/src/main/res/layout/item_other_session.xml b/vector/src/main/res/layout/item_other_session.xml index f514cea56b..dee96d2b2f 100644 --- a/vector/src/main/res/layout/item_other_session.xml +++ b/vector/src/main/res/layout/item_other_session.xml @@ -67,12 +67,23 @@ android:layout_height="wrap_content" android:layout_marginTop="2dp" android:drawablePadding="8dp" - app:layout_constraintBottom_toBottomOf="@id/otherSessionDeviceTypeImageView" app:layout_constraintEnd_toEndOf="@id/otherSessionNameTextView" app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView" app:layout_constraintTop_toBottomOf="@id/otherSessionNameTextView" tools:text="@string/device_manager_verification_status_verified" /> + + + app:layout_constraintTop_toBottomOf="@id/otherSessionIpAddressTextView" /> From b5e8375592d43aaebfbcc20a6efe8694170f3668 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 8 Nov 2022 15:16:09 +0300 Subject: [PATCH 159/679] Toggle ip address on other sessions screen. --- .../v2/othersessions/OtherSessionsAction.kt | 1 + .../v2/othersessions/OtherSessionsFragment.kt | 21 ++++++++++++++++--- .../othersessions/OtherSessionsViewModel.kt | 8 +++++++ .../othersessions/OtherSessionsViewState.kt | 1 + .../src/main/res/menu/menu_other_sessions.xml | 5 +++++ 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index 24d2a08bdc..f249a9f0b6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -33,4 +33,5 @@ sealed class OtherSessionsAction : VectorViewModelAction { object SelectAll : OtherSessionsAction() object DeselectAll : OtherSessionsAction() object MultiSignout : OtherSessionsAction() + object ToggleIpAddressVisibility: OtherSessionsAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 487531646a..c2beeb4db5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -85,6 +85,12 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() + menu.findItem(R.id.otherSessionsToggleIpAddress).isVisible = !isSelectModeEnabled + menu.findItem(R.id.otherSessionsToggleIpAddress).title = if (state.isShowingIpAddress) { + getString(R.string.device_manager_other_sessions_hide_ip_address) + } else { + getString(R.string.device_manager_other_sessions_show_ip_address) + } updateMultiSignoutMenuItem(menu, state) } } @@ -130,10 +136,18 @@ class OtherSessionsFragment : confirmMultiSignout() true } + R.id.otherSessionsToggleIpAddress -> { + toggleIpAddressVisibility() + true + } else -> false } } + private fun toggleIpAddressVisibility() { + viewModel.handle(OtherSessionsAction.ToggleIpAddressVisibility) + } + private fun confirmMultiSignout() { activity?.let { buildConfirmSignoutDialogUseCase.execute(it, this::multiSignout) @@ -213,7 +227,7 @@ class OtherSessionsFragment : updateLoading(state.isLoading) if (state.devices is Success) { val devices = state.devices.invoke() - renderDevices(devices, state.currentFilter) + renderDevices(devices, state.currentFilter, state.isShowingIpAddress) updateToolbar(devices, state.isSelectModeEnabled) } } @@ -237,7 +251,7 @@ class OtherSessionsFragment : toolbar?.title = title } - private fun renderDevices(devices: List, currentFilter: DeviceManagerFilterType) { + private fun renderDevices(devices: List, currentFilter: DeviceManagerFilterType, isShowingIpAddress: Boolean) { views.otherSessionsFilterBadgeImageView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS views.otherSessionsSecurityRecommendationView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS views.deviceListHeaderOtherSessions.isVisible = currentFilter == DeviceManagerFilterType.ALL_SESSIONS @@ -296,7 +310,8 @@ class OtherSessionsFragment : } else { views.deviceListOtherSessions.isVisible = true views.otherSessionsNotFoundLayout.isVisible = false - views.deviceListOtherSessions.render(devices = devices, totalNumberOfDevices = devices.size, showViewAll = false) + val mappedDevices = if (isShowingIpAddress) devices else devices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) } + views.deviceListOtherSessions.render(devices = mappedDevices, totalNumberOfDevices = mappedDevices.size, showViewAll = false) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index c33490400b..d1302cf443 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -91,6 +91,14 @@ class OtherSessionsViewModel @AssistedInject constructor( OtherSessionsAction.DeselectAll -> handleDeselectAll() OtherSessionsAction.SelectAll -> handleSelectAll() OtherSessionsAction.MultiSignout -> handleMultiSignout() + OtherSessionsAction.ToggleIpAddressVisibility -> handleToggleIpAddressVisibility() + } + } + + private fun handleToggleIpAddressVisibility() = withState { state -> + val isShowingIpAddress = state.isShowingIpAddress + setState { + copy(isShowingIpAddress = !isShowingIpAddress) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt index c0b50fded8..f4dd3640ee 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt @@ -28,6 +28,7 @@ data class OtherSessionsViewState( val excludeCurrentDevice: Boolean = false, val isSelectModeEnabled: Boolean = false, val isLoading: Boolean = false, + val isShowingIpAddress: Boolean = false, ) : MavericksState { constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice) diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index 7893575dde..98f9dd8256 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -9,6 +9,11 @@ android:title="@string/device_manager_other_sessions_select" app:showAsAction="withText|never" /> + + Date: Tue, 8 Nov 2022 16:33:20 +0300 Subject: [PATCH 160/679] Toggle ip address on sessions overview screen. --- .../devices/v2/list/SessionInfoView.kt | 3 +++ .../devices/v2/list/SessionInfoViewState.kt | 1 + .../v2/overview/SessionOverviewAction.kt | 1 + .../v2/overview/SessionOverviewFragment.kt | 20 +++++++++++++++++++ .../v2/overview/SessionOverviewViewModel.kt | 8 ++++++++ .../v2/overview/SessionOverviewViewState.kt | 1 + .../main/res/menu/menu_session_overview.xml | 5 +++++ 7 files changed, 39 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 3d9c3a8f37..6e7e57fc49 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -76,6 +76,7 @@ class SessionInfoView @JvmOverloads constructor( sessionInfoViewState.deviceFullInfo.isInactive, sessionInfoViewState.deviceFullInfo.deviceInfo, sessionInfoViewState.isLastSeenDetailsVisible, + sessionInfoViewState.isShowingIpAddress, dateFormatter, drawableProvider, colorProvider, @@ -157,6 +158,7 @@ class SessionInfoView @JvmOverloads constructor( isInactive: Boolean, deviceInfo: DeviceInfo, isLastSeenDetailsVisible: Boolean, + isShowingIpAddress: Boolean, dateFormatter: VectorDateFormatter, drawableProvider: DrawableProvider, colorProvider: ColorProvider, @@ -187,6 +189,7 @@ class SessionInfoView @JvmOverloads constructor( views.sessionInfoLastActivityTextView.isGone = true } views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible }) + views.sessionInfoLastIPAddressTextView.isVisible = isShowingIpAddress } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 287bb956f5..5d3c4b4f4b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -25,4 +25,5 @@ data class SessionInfoViewState( val isDetailsButtonVisible: Boolean = true, val isLearnMoreLinkVisible: Boolean = false, val isLastSeenDetailsVisible: Boolean = false, + val isShowingIpAddress: Boolean = false, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt index 9a92d5b629..2b6c40eead 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt @@ -29,4 +29,5 @@ sealed class SessionOverviewAction : VectorViewModelAction { val deviceId: String, val enabled: Boolean, ) : SessionOverviewAction() + object ToggleIpAddressVisibility : SessionOverviewAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index e149023f22..61f7ab39dd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2.overview import android.app.Activity import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -156,16 +157,34 @@ class SessionOverviewFragment : override fun getMenuRes() = R.menu.menu_session_overview + override fun handlePrepareMenu(menu: Menu) { + withState(viewModel) { state -> + menu.findItem(R.id.sessionOverviewToggleIpAddress).title = if (state.isShowingIpAddress) { + getString(R.string.device_manager_other_sessions_hide_ip_address) + } else { + getString(R.string.device_manager_other_sessions_show_ip_address) + } + } + } + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.sessionOverviewRename -> { goToRenameSession() true } + R.id.sessionOverviewToggleIpAddress -> { + toggleIpAddressVisibility() + true + } else -> false } } + private fun toggleIpAddressVisibility() { + viewModel.handle(SessionOverviewAction.ToggleIpAddressVisibility) + } + private fun goToRenameSession() = withState(viewModel) { state -> viewNavigator.goToRenameSession(requireContext(), state.deviceId) } @@ -206,6 +225,7 @@ class SessionOverviewFragment : isDetailsButtonVisible = false, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, isLastSeenDetailsVisible = !isCurrentSession, + isShowingIpAddress = viewState.isShowingIpAddress, ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) views.sessionOverviewInfo.onLearnMoreClickListener = { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 59eeaaadb4..74ab5ce617 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -113,6 +113,14 @@ class SessionOverviewViewModel @AssistedInject constructor( is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action) SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled() is SessionOverviewAction.TogglePushNotifications -> handleTogglePusherAction(action) + SessionOverviewAction.ToggleIpAddressVisibility -> handleToggleIpAddressVisibility() + } + } + + private fun handleToggleIpAddressVisibility() = withState { state -> + val isShowingIpAddress = state.isShowingIpAddress + setState { + copy(isShowingIpAddress = !isShowingIpAddress) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt index 019dd2d724..0f66605f98 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt @@ -28,6 +28,7 @@ data class SessionOverviewViewState( val deviceInfo: Async = Uninitialized, val isLoading: Boolean = false, val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED, + val isShowingIpAddress: Boolean = false, ) : MavericksState { constructor(args: SessionOverviewArgs) : this( deviceId = args.deviceId diff --git a/vector/src/main/res/menu/menu_session_overview.xml b/vector/src/main/res/menu/menu_session_overview.xml index 7de3953dcc..4179a0d975 100644 --- a/vector/src/main/res/menu/menu_session_overview.xml +++ b/vector/src/main/res/menu/menu_session_overview.xml @@ -4,6 +4,11 @@ xmlns:tools="http://schemas.android.com/tools" tools:ignore="AlwaysShowAction"> + + Date: Tue, 8 Nov 2022 17:43:48 +0300 Subject: [PATCH 161/679] Persist user preference of ip address visibility. --- .../features/settings/VectorPreferences.kt | 3 +++ .../settings/devices/v2/DevicesViewModel.kt | 21 ++++++++++++++++++- .../othersessions/OtherSessionsViewModel.kt | 19 ++++++++++++++++- .../v2/overview/SessionOverviewViewModel.kt | 17 +++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 9f40a7cede..3424c2b54c 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -209,6 +209,9 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG" const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG" + // New Session Manager + const val SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS = "SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS" + // other const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" private const val SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY" diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 3cacf82f14..bf42867b38 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -16,15 +16,19 @@ package im.vector.app.features.settings.devices.v2 +import android.content.SharedPreferences +import androidx.core.content.edit import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult @@ -53,6 +57,8 @@ class DevicesViewModel @AssistedInject constructor( private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, + @DefaultPreferences + private val sharedPreferences: SharedPreferences, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @AssistedFactory @@ -67,6 +73,14 @@ class DevicesViewModel @AssistedInject constructor( observeDevices() refreshDevicesOnCryptoDevicesChange() refreshDeviceList() + refreshIpAddressVisibility() + } + + private fun refreshIpAddressVisibility() { + val shouldShowIpAddress = sharedPreferences.getBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, false) + setState { + copy(isShowingIpAddress = shouldShowIpAddress) + } } private fun observeCurrentSessionCrossSigningInfo() { @@ -122,7 +136,12 @@ class DevicesViewModel @AssistedInject constructor( private fun handleToggleIpAddressVisibility() = withState { state -> val isShowingIpAddress = state.isShowingIpAddress - setState { copy(isShowingIpAddress = !isShowingIpAddress) } + setState { + copy(isShowingIpAddress = !isShowingIpAddress) + } + sharedPreferences.edit { + putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, !isShowingIpAddress) + } } private fun handleVerifyCurrentSessionAction() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index d1302cf443..7cca2e54e1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -16,15 +16,19 @@ package im.vector.app.features.settings.devices.v2.othersessions +import android.content.SharedPreferences +import androidx.core.content.edit import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel @@ -48,7 +52,9 @@ class OtherSessionsViewModel @AssistedInject constructor( private val signoutSessionsUseCase: SignoutSessionsUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, - refreshDevicesUseCase: RefreshDevicesUseCase + refreshDevicesUseCase: RefreshDevicesUseCase, + @DefaultPreferences + private val sharedPreferences: SharedPreferences, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase ) { @@ -64,6 +70,14 @@ class OtherSessionsViewModel @AssistedInject constructor( init { observeDevices(initialState.currentFilter) + refreshIpAddressVisibility() + } + + private fun refreshIpAddressVisibility() { + val shouldShowIpAddress = sharedPreferences.getBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, false) + setState { + copy(isShowingIpAddress = shouldShowIpAddress) + } } private fun observeDevices(currentFilter: DeviceManagerFilterType) { @@ -100,6 +114,9 @@ class OtherSessionsViewModel @AssistedInject constructor( setState { copy(isShowingIpAddress = !isShowingIpAddress) } + sharedPreferences.edit { + putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, !isShowingIpAddress) + } } private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 74ab5ce617..a344474eba 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -16,15 +16,19 @@ package im.vector.app.features.settings.devices.v2.overview +import android.content.SharedPreferences +import androidx.core.content.edit import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase @@ -58,6 +62,8 @@ class SessionOverviewViewModel @AssistedInject constructor( private val togglePushNotificationUseCase: TogglePushNotificationUseCase, private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, + @DefaultPreferences + private val sharedPreferences: SharedPreferences, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase ) { @@ -74,6 +80,14 @@ class SessionOverviewViewModel @AssistedInject constructor( observeSessionInfo(initialState.deviceId) observeCurrentSessionInfo() observeNotificationsStatus(initialState.deviceId) + refreshIpAddressVisibility() + } + + private fun refreshIpAddressVisibility() { + val shouldShowIpAddress = sharedPreferences.getBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, false) + setState { + copy(isShowingIpAddress = shouldShowIpAddress) + } } private fun refreshPushers() { @@ -122,6 +136,9 @@ class SessionOverviewViewModel @AssistedInject constructor( setState { copy(isShowingIpAddress = !isShowingIpAddress) } + sharedPreferences.edit { + putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, !isShowingIpAddress) + } } private fun handleVerifySessionAction() = withState { viewState -> From c74445cf5bb0c5ad67e1b0c20aeb4011abf5e0b4 Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Thu, 3 Nov 2022 10:13:05 +0000 Subject: [PATCH 162/679] Update PR automation Stop using deprecated ProjectNext API in favour of the new ProjectV2 one --- .github/workflows/triage-move-review-requests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml index 61f1f114dd..6aeba66ccc 100644 --- a/.github/workflows/triage-move-review-requests.yml +++ b/.github/workflows/triage-move-review-requests.yml @@ -60,8 +60,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -129,8 +129,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } From 6073cf12da5a5b75989af416def2096453e455fc Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Thu, 3 Nov 2022 10:07:33 +0000 Subject: [PATCH 163/679] Update issue automation Stop using deprecated ProjectNext API in favour of the new ProjectV2 one --- .github/workflows/triage-labelled.yml | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 8e9cc6d76c..f1458a1d11 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -79,8 +79,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -103,8 +103,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -129,8 +129,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -154,8 +154,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -178,8 +178,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -203,8 +203,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -228,8 +228,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -258,8 +258,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } From 8bc70002d9eb047cdf39917a59b76b7173252c1b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 8 Nov 2022 19:31:59 +0300 Subject: [PATCH 164/679] Add changelog. --- changelog.d/7546.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7546.feature diff --git a/changelog.d/7546.feature b/changelog.d/7546.feature new file mode 100644 index 0000000000..94450082c9 --- /dev/null +++ b/changelog.d/7546.feature @@ -0,0 +1 @@ +[Device Manager] Toggle IP address visibility From e888c1174726dedd0029efd004f57c1bda2ee71e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 8 Nov 2022 20:05:16 +0300 Subject: [PATCH 165/679] Lint fix. --- .../settings/devices/v2/othersessions/OtherSessionsAction.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index f249a9f0b6..bdad65ca43 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -33,5 +33,5 @@ sealed class OtherSessionsAction : VectorViewModelAction { object SelectAll : OtherSessionsAction() object DeselectAll : OtherSessionsAction() object MultiSignout : OtherSessionsAction() - object ToggleIpAddressVisibility: OtherSessionsAction() + object ToggleIpAddressVisibility : OtherSessionsAction() } From 06538276d9948dd467cd1bce88a7c11d598af6f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:04:44 +0000 Subject: [PATCH 166/679] Bump kotlin-gradle-plugin from 1.7.20 to 1.7.21 Bumps [kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) from 1.7.20 to 1.7.21. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/commits) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 751cba4d44..cc5cb73586 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -8,7 +8,7 @@ ext.versions = [ def gradle = "7.3.1" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.7.20" +def kotlin = "1.7.21" def kotlinCoroutines = "1.6.4" def dagger = "2.44" def appDistribution = "16.0.0-beta05" From 25d33e9b1aebf4c39e66cd89e8ff554bc7010d4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:11:03 +0000 Subject: [PATCH 167/679] Bump kotlin-reflect from 1.7.20 to 1.7.21 Bumps [kotlin-reflect](https://github.com/JetBrains/kotlin) from 1.7.20 to 1.7.21. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/commits) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index f793fff2c8..612bda56c0 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -412,7 +412,7 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" debugImplementation libs.androidx.fragmentTesting debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' } diff --git a/vector/build.gradle b/vector/build.gradle index 1ce78520f2..c35f21163f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -331,5 +331,5 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.20" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" } From cd08b8134cd3153c6cef2f6621fa31abe25aefdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:12:24 +0000 Subject: [PATCH 168/679] Bump orchestrator from 1.4.1 to 1.4.2 Bumps orchestrator from 1.4.1 to 1.4.2. --- updated-dependencies: - dependency-name: androidx.test:orchestrator dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 751cba4d44..f3b9c5cc2e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -32,7 +32,7 @@ def fragment = "1.5.4" def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.4.0" def androidxTest = "1.4.0" -def androidxOrchestrator = "1.4.1" +def androidxOrchestrator = "1.4.2" def paparazzi = "1.1.0" ext.libs = [ From 46c60f5897670e98c8e477477599afce631304b8 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Nov 2022 16:57:16 +0300 Subject: [PATCH 169/679] Fix unit tests. --- .../app/features/settings/devices/v2/DevicesViewModelTest.kt | 4 ++++ .../devices/v2/othersessions/OtherSessionsViewModelTest.kt | 4 ++++ .../devices/v2/overview/SessionOverviewViewModelTest.kt | 4 ++++ .../java/im/vector/app/test/fakes/FakeSharedPreferences.kt | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 852fc64fd5..87bae3abb7 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -28,6 +28,7 @@ import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCro import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSharedPreferences import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -72,6 +73,7 @@ class DevicesViewModelTest { private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) + private val fakeSharedPreferences = FakeSharedPreferences() private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -85,6 +87,7 @@ class DevicesViewModelTest { interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, + sharedPreferences = fakeSharedPreferences, ) } @@ -97,6 +100,7 @@ class DevicesViewModelTest { givenVerificationService() givenCurrentSessionCrossSigningInfo() givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + fakeSharedPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index e01d6e058c..e7bb14695c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -26,6 +26,7 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSharedPreferences import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo @@ -68,6 +69,7 @@ class OtherSessionsViewModelTest { private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() + private val fakeSharedPreferences = FakeSharedPreferences() private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( @@ -78,6 +80,7 @@ class OtherSessionsViewModelTest { interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, + sharedPreferences = fakeSharedPreferences, ) @Before @@ -87,6 +90,7 @@ class OtherSessionsViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() + fakeSharedPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index b2ab939bd1..aec555d8eb 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -28,6 +28,7 @@ import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowRe import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSharedPreferences import im.vector.app.test.fakes.FakeSignoutSessionUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService @@ -77,6 +78,7 @@ class SessionOverviewViewModelTest { private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() private val fakeGetNotificationsStatusUseCase = mockk() private val notificationsStatus = NotificationsStatus.ENABLED + private val fakeSharedPreferences = FakeSharedPreferences() private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), @@ -89,6 +91,7 @@ class SessionOverviewViewModelTest { refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase, + sharedPreferences = fakeSharedPreferences, ) @Before @@ -99,6 +102,7 @@ class SessionOverviewViewModelTest { givenVerificationService() every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) + fakeSharedPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt index f9d525fd13..0242bfe148 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt @@ -18,6 +18,7 @@ package im.vector.app.test.fakes import android.content.SharedPreferences import im.vector.app.features.settings.FontScaleValue +import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS import io.mockk.every import io.mockk.mockk @@ -32,4 +33,8 @@ class FakeSharedPreferences : SharedPreferences by mockk() { every { contains("APPLICATION_USE_SYSTEM_FONT_SCALE_KEY") } returns true every { getBoolean("APPLICATION_USE_SYSTEM_FONT_SCALE_KEY", any()) } returns useSystemScale } + + fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { + every { getBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, any()) } returns showIpAddress + } } From ba6d414f67483bc13001409e13d54f25e84dd8ba Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Nov 2022 16:59:02 +0300 Subject: [PATCH 170/679] Code review fix. --- library/ui-strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 370005363d..5a37cc7f7c 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3375,7 +3375,7 @@ Verified sessions Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. - Verified sessions are anywhere you are using ${app_name} after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. + Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. Renaming sessions Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. Enable new session manager From 823e7bf212058540a31e61bc62a6b770d5b23392 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Nov 2022 15:26:39 +0100 Subject: [PATCH 171/679] Fix search tests. --- .../matrix/android/sdk/session/search/SearchMessagesTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 6ef90193d8..66929dcf31 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -43,7 +43,7 @@ class SearchMessagesTest : InstrumentedTest { cryptoTestData.firstSession .searchService() .search( - searchTerm = "lore", + searchTerm = "lorem", limit = 10, includeProfile = true, afterLimit = 0, @@ -61,7 +61,7 @@ class SearchMessagesTest : InstrumentedTest { cryptoTestData.firstSession .searchService() .search( - searchTerm = "lore", + searchTerm = "lorem", roomId = cryptoTestData.roomId, limit = 10, includeProfile = true, From d07c6da3ac4b2ecb996b1e588af81b4392e1fd0a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Nov 2022 15:31:27 +0100 Subject: [PATCH 172/679] Add a test for incomplete word. --- .../sdk/session/search/SearchMessagesTest.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 66929dcf31..81351523e9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.search +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Test @@ -73,7 +74,28 @@ class SearchMessagesTest : InstrumentedTest { } } - private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + @Test + fun sendTextMessageAndSearchPartOfItIncompleteWord() { + doTest(expectedNumberOfResult = 0) { cryptoTestData -> + cryptoTestData.firstSession + .searchService() + .search( + searchTerm = "lore", /* incomplete word */ + roomId = cryptoTestData.roomId, + limit = 10, + includeProfile = true, + afterLimit = 0, + beforeLimit = 10, + orderByRecent = true, + nextBatch = null + ) + } + } + + private fun doTest( + expectedNumberOfResult: Int = 2, + block: suspend (CryptoTestData) -> SearchResult, + ) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -87,7 +109,7 @@ class SearchMessagesTest : InstrumentedTest { val data = block.invoke(cryptoTestData) - assertTrue(data.results?.size == 2) + data.results?.size shouldBeEqualTo expectedNumberOfResult assertTrue( data.results ?.all { From 40e960f19e23201fc83e11b24f800e38daa5ccd6 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 9 Nov 2022 20:41:53 +0300 Subject: [PATCH 173/679] Lint fix. --- library/ui-strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index b8fc5e4fbd..f05a2a11e6 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3379,7 +3379,7 @@ Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. Verified sessions - Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. + Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. Renaming sessions Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. From 02c16d30f41ddc3fa43ff31cf36de19b6ef44e6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 23:03:07 +0000 Subject: [PATCH 174/679] Bump com.google.devtools.ksp from 1.7.20-1.0.7 to 1.7.21-1.0.8 Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 1.7.20-1.0.7 to 1.7.21-1.0.8. - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/1.7.20-1.0.7...1.7.21-1.0.8) --- updated-dependencies: - dependency-name: com.google.devtools.ksp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7e7da48295..5e5db64834 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ plugins { // Detekt id "io.gitlab.arturbosch.detekt" version "1.21.0" // Ksp - id "com.google.devtools.ksp" version "1.7.20-1.0.7" + id "com.google.devtools.ksp" version "1.7.21-1.0.8" // Dependency Analysis id 'com.autonomousapps.dependency-analysis' version "1.13.1" From e84c68495ff721c3e850ee970d3f9196e6cfa597 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 23:03:47 +0000 Subject: [PATCH 175/679] Bump posthog from 1.1.2 to 2.0.0 Bumps [posthog](https://github.com/PostHog/posthog-android) from 1.1.2 to 2.0.0. - [Release notes](https://github.com/PostHog/posthog-android/releases) - [Changelog](https://github.com/PostHog/posthog-android/blob/master/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-android/compare/1.1.2...2.0.0) --- updated-dependencies: - dependency-name: com.posthog.android:posthog dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- vector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/build.gradle b/vector/build.gradle index 1ce78520f2..76518b4380 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -233,7 +233,7 @@ dependencies { kapt libs.dagger.hiltCompiler // Analytics - implementation('com.posthog.android:posthog:1.1.2') { + implementation('com.posthog.android:posthog:2.0.0') { exclude group: 'com.android.support', module: 'support-annotations' } implementation libs.sentry.sentryAndroid From 41ab29d4c008c7abd61db0f6cdc860cf01e894e9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 2 Nov 2022 17:17:44 +0100 Subject: [PATCH 176/679] Adding changelog entry --- changelog.d/7512.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7512.feature diff --git a/changelog.d/7512.feature b/changelog.d/7512.feature new file mode 100644 index 0000000000..00411a75ad --- /dev/null +++ b/changelog.d/7512.feature @@ -0,0 +1 @@ +Push notifications toggle: align implementation for current session From 2941cfa329519de3248e437f7e92de3c5d7ab802 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 11:39:56 +0100 Subject: [PATCH 177/679] Adding use cases to handle toggle of push notifications for current session --- .../src/main/res/values/strings.xml | 3 +- .../vector/app/core/pushers/PushersManager.kt | 6 -- ...leNotificationsForCurrentSessionUseCase.kt | 44 ++++++++++++ ...leNotificationsForCurrentSessionUseCase.kt | 72 +++++++++++++++++++ ...rSettingsNotificationPreferenceFragment.kt | 32 +++------ 5 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f05a2a11e6..372692770e 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -1679,7 +1679,8 @@ Create New Room Create New Space No network. Please check your Internet connection. - Something went wrong. Please check your network connection and try again. + + Something went wrong. Please check your network connection and try again. "Change network" "Please wait…" Updating your data… diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index cda6f5bae8..6f186262fc 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -97,12 +97,6 @@ class PushersManager @Inject constructor( return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } } - suspend fun togglePusherForCurrentSession(enable: Boolean) { - val session = activeSessionHolder.getSafeActiveSession() ?: return - val pusher = getPusherForCurrentSession() ?: return - session.pushersService().togglePusher(pusher, enable) - } - suspend fun unregisterEmailPusher(email: String) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return currentSession.pushersService().removeEmailPusher(email) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt new file mode 100644 index 0000000000..8962e8d67d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import javax.inject.Inject + +class DisableNotificationsForCurrentSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, + private val pushersManager: PushersManager, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val togglePushNotificationUseCase: TogglePushNotificationUseCase, +) { + + // TODO add unit tests + suspend fun execute() { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val deviceId = session.sessionParams.deviceId ?: return + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + togglePushNotificationUseCase.execute(deviceId, enabled = false) + } else { + unifiedPushHelper.unregister(pushersManager) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt new file mode 100644 index 0000000000..ef37f67bef --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import androidx.fragment.app.FragmentActivity +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.FcmHelper +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class EnableNotificationsForCurrentSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, + private val pushersManager: PushersManager, + private val fcmHelper: FcmHelper, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val togglePushNotificationUseCase: TogglePushNotificationUseCase, +) { + + // TODO add unit tests + suspend fun execute(fragmentActivity: FragmentActivity) { + val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() + if (pusherForCurrentSession == null) { + registerPusher(fragmentActivity) + } + + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val deviceId = session.sessionParams.deviceId ?: return + togglePushNotificationUseCase.execute(deviceId, enabled = true) + } + } + + private suspend fun registerPusher(fragmentActivity: FragmentActivity) { + suspendCoroutine { continuation -> + try { + unifiedPushHelper.register(fragmentActivity) { + if (unifiedPushHelper.isEmbeddedDistributor()) { + fcmHelper.ensureFcmTokenIsRetrieved( + fragmentActivity, + pushersManager, + registerPusher = true + ) + } + continuation.resume(Unit) + } + } catch (error: Exception) { + continuation.resumeWithException(error) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index f800c518f3..4a43a20de3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -57,7 +57,6 @@ import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session @@ -81,6 +80,8 @@ class VectorSettingsNotificationPreferenceFragment : @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var vectorFeatures: VectorFeatures @Inject lateinit var notificationPermissionManager: NotificationPermissionManager + @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase + @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase override var titleRes: Int = R.string.settings_notifications override val preferenceXmlRes = R.xml.vector_settings_notifications @@ -126,28 +127,12 @@ class VectorSettingsNotificationPreferenceFragment : it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { - unifiedPushHelper.register(requireActivity()) { - // Update the summary - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - requireActivity(), - pushersManager, - vectorPreferences.areNotificationEnabledForDevice() - ) - } - findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) - ?.summary = unifiedPushHelper.getCurrentDistributorName() - lifecycleScope.launch { - val result = runCatching { - pushersManager.togglePusherForCurrentSession(true) - } + enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) - result.exceptionOrNull()?.let { _ -> - Toast.makeText(context, R.string.error_check_network, Toast.LENGTH_SHORT).show() - it.isChecked = false - } - } - } + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() + + // TODO test with API 33 notificationPermissionManager.eventuallyRequestPermission( requireActivity(), postPermissionLauncher, @@ -155,8 +140,7 @@ class VectorSettingsNotificationPreferenceFragment : ignorePreference = true ) } else { - unifiedPushHelper.unregister(pushersManager) - session.pushersService().refreshPushers() + disableNotificationsForCurrentSessionUseCase.execute() notificationPermissionManager.eventuallyRevokePermission(requireActivity()) } } From 67d2a6faab0979c48e1b9d9f43de8565d06e0635 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 16:31:41 +0100 Subject: [PATCH 178/679] Use the preference value to render the push notifications toggle --- ...rSettingsNotificationPreferenceFragment.kt | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 4a43a20de3..58f86bc949 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -120,31 +120,25 @@ class VectorSettingsNotificationPreferenceFragment : (pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel } - findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { - pushersManager.getPusherForCurrentSession()?.let { pusher -> - it.isChecked = pusher.enabled - } - - it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> - if (isChecked) { - enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) - - findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) - ?.summary = unifiedPushHelper.getCurrentDistributorName() - - // TODO test with API 33 - notificationPermissionManager.eventuallyRequestPermission( - requireActivity(), - postPermissionLauncher, - showRationale = false, - ignorePreference = true - ) - } else { - disableNotificationsForCurrentSessionUseCase.execute() - notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + ?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> + if (isChecked) { + enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) + + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() + + notificationPermissionManager.eventuallyRequestPermission( + requireActivity(), + postPermissionLauncher, + showRationale = false, + ignorePreference = true + ) + } else { + disableNotificationsForCurrentSessionUseCase.execute() + notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + } } - } - } findPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { From 24a5cfa9e54fed6b9d9aaebd5f9471222e883786 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 16:33:16 +0100 Subject: [PATCH 179/679] Listen for pusher or account data changes to update the local setting --- .../HomeServerCapabilitiesService.kt | 8 +++ .../DefaultHomeServerCapabilitiesService.kt | 6 +++ .../HomeServerCapabilitiesDataSource.kt | 17 +++++- .../vector/app/core/di/ActiveSessionHolder.kt | 1 + .../EnableNotificationsSettingUpdater.kt | 41 ++++++++++++++ ...ableNotificationsSettingOnChangeUseCase.kt | 53 +++++++++++++++++++ .../ConfigureAndStartSessionUseCase.kt | 4 ++ ...TogglePushNotificationsViaPusherUseCase.kt | 36 +++++++++++++ ...ePushNotificationsViaAccountDataUseCase.kt | 15 +++--- ...TogglePushNotificationsViaPusherUseCase.kt | 19 +++---- .../GetNotificationsStatusUseCase.kt | 34 ++++++------ .../TogglePushNotificationUseCase.kt | 4 +- .../v2/overview/SessionOverviewViewModel.kt | 8 +-- ...leNotificationsForCurrentSessionUseCase.kt | 2 +- ...leNotificationsForCurrentSessionUseCase.kt | 4 +- 15 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt create mode 100644 vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt index 9d2c48e194..c65a5382fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.api.session.homeserver +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.util.Optional + /** * This interface defines a method to retrieve the homeserver capabilities. */ @@ -30,4 +33,9 @@ interface HomeServerCapabilitiesService { * Get the HomeServer capabilities. */ fun getHomeServerCapabilities(): HomeServerCapabilities + + /** + * Get a LiveData on the HomeServer capabilities. + */ + fun getHomeServerCapabilitiesLive(): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt index 4c755b54b5..eb9e862de2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.internal.session.homeserver +import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.util.Optional import javax.inject.Inject internal class DefaultHomeServerCapabilitiesService @Inject constructor( @@ -33,4 +35,8 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor( return homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities() } + + override fun getHomeServerCapabilitiesLive(): LiveData> { + return homeServerCapabilitiesDataSource.getHomeServerCapabilitiesLive() + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt index 6c913fa41e..beb1e67e40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt @@ -16,9 +16,14 @@ package org.matrix.android.sdk.internal.session.homeserver +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import io.realm.Realm +import io.realm.kotlin.where import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity import org.matrix.android.sdk.internal.database.query.get @@ -26,7 +31,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import javax.inject.Inject internal class HomeServerCapabilitiesDataSource @Inject constructor( - @SessionDatabase private val monarchy: Monarchy + @SessionDatabase private val monarchy: Monarchy, ) { fun getHomeServerCapabilities(): HomeServerCapabilities? { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> @@ -35,4 +40,14 @@ internal class HomeServerCapabilitiesDataSource @Inject constructor( } } } + + fun getHomeServerCapabilitiesLive(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> realm.where() }, + { HomeServerCapabilitiesMapper.map(it) } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } } diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 7e4f73e7a5..1e9f080303 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -19,6 +19,7 @@ package im.vector.app.core.di import android.content.Context import im.vector.app.ActiveSessionDataSource import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.session.ConfigureAndStartSessionUseCase diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt new file mode 100644 index 0000000000..21febaee9d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.notification + +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class EnableNotificationsSettingUpdater @Inject constructor( + private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase, +) { + + private var job: Job? = null + + fun onSessionsStarted(session: Session) { + job?.cancel() + job = session.coroutineScope.launch { + updateEnableNotificationsSettingOnChangeUseCase.execute(session) + .launchIn(this) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt new file mode 100644 index 0000000000..a6bcf17b5c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.notification + +import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +/** + * Listen for changes in either Pusher or Account data to update the local enable notifications + * setting for the current device. + */ +class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, +) { + + // TODO add unit tests + fun execute(session: Session): Flow { + val deviceId = session.sessionParams.deviceId ?: return emptyFlow() + return getNotificationsStatusUseCase.execute(session, deviceId) + .onEach(::updatePreference) + } + + private fun updatePreference(notificationStatus: NotificationsStatus) { + Timber.d("updatePreference with status=$notificationStatus") + when (notificationStatus) { + NotificationsStatus.ENABLED -> vectorPreferences.setNotificationEnabledForDevice(true) + NotificationsStatus.DISABLED -> vectorPreferences.setNotificationEnabledForDevice(false) + else -> Unit + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index a5e1fe68bd..00dc1ab5f9 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -19,6 +19,7 @@ package im.vector.app.core.session import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.settings.VectorPreferences @@ -32,8 +33,10 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val webRtcCallManager: WebRtcCallManager, private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase, private val vectorPreferences: VectorPreferences, + private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, ) { + // TODO update unit tests suspend fun execute(session: Session, startSyncing: Boolean = true) { Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") session.open() @@ -46,5 +49,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor( if (vectorPreferences.isClientInfoRecordingEnabled()) { updateMatrixClientInfoUseCase.execute(session) } + enableNotificationsSettingUpdater.onSessionsStarted(session) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt new file mode 100644 index 0000000000..963768ca04 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import androidx.lifecycle.asFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.flow.unwrap +import javax.inject.Inject + +class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { + + fun execute(session: Session): Flow { + return session + .homeServerCapabilitiesService() + .getHomeServerCapabilitiesLive() + .asFlow() + .unwrap() + .map { it.canRemotelyTogglePushNotificationsOfDevices } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt index dbf9adca14..194a2aebbf 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -16,18 +16,15 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { +class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { - fun execute(deviceId: String): Boolean { - return activeSessionHolder - .getSafeActiveSession() - ?.accountDataService() - ?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null + fun execute(session: Session, deviceId: String): Boolean { + return session + .accountDataService() + .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt index 0d5bce663a..ca314bf145 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -16,20 +16,15 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.core.di.ActiveSessionHolder -import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { +class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { - fun execute(): Boolean { - return activeSessionHolder - .getSafeActiveSession() - ?.homeServerCapabilitiesService() - ?.getHomeServerCapabilities() - ?.canRemotelyTogglePushNotificationsOfDevices - .orFalse() + fun execute(session: Session): Boolean { + return session + .homeServerCapabilitiesService() + .getHomeServerCapabilities() + .canRemotelyTogglePushNotificationsOfDevices } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 69659bf23f..03e4e31f2e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -16,12 +16,13 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.flow.flow @@ -29,16 +30,13 @@ import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject class GetNotificationsStatusUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase, private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - fun execute(deviceId: String): Flow { - val session = activeSessionHolder.getSafeActiveSession() + fun execute(session: Session, deviceId: String): Flow { return when { - session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) - checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { + checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { session.flow() .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) .unwrap() @@ -46,15 +44,19 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } .distinctUntilChanged() } - checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { - session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } - .distinctUntilChanged() - } - else -> flowOf(NotificationsStatus.NOT_SUPPORTED) + else -> canTogglePushNotificationsViaPusherUseCase.execute(session) + .flatMapLatest { canToggle -> + if (canToggle) { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() + } else { + flowOf(NotificationsStatus.NOT_SUPPORTED) + } + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index be9012e9f1..7969bbbe9b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -31,14 +31,14 @@ class TogglePushNotificationUseCase @Inject constructor( suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } devicePusher?.let { pusher -> session.pushersService().togglePusher(pusher, enabled) } } - if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) { + if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) session.accountDataService().updateUserAccountData( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 9c4ece7e02..a56872e648 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -96,9 +96,11 @@ class SessionOverviewViewModel @AssistedInject constructor( } private fun observeNotificationsStatus(deviceId: String) { - getNotificationsStatusUseCase.execute(deviceId) - .onEach { setState { copy(notificationsStatus = it) } } - .launchIn(viewModelScope) + activeSessionHolder.getSafeActiveSession()?.let { session -> + getNotificationsStatusUseCase.execute(session, deviceId) + .onEach { setState { copy(notificationsStatus = it) } } + .launchIn(viewModelScope) + } } override fun handle(action: SessionOverviewAction) { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 8962e8d67d..d66bf8b789 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -35,7 +35,7 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor( suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { togglePushNotificationUseCase.execute(deviceId, enabled = false) } else { unifiedPushHelper.unregister(pushersManager) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index ef37f67bef..cee653380a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -44,8 +44,8 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( registerPusher(fragmentActivity) } - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { - val session = activeSessionHolder.getSafeActiveSession() ?: return + val session = activeSessionHolder.getSafeActiveSession() ?: return + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { val deviceId = session.sessionParams.deviceId ?: return togglePushNotificationUseCase.execute(deviceId, enabled = true) } From 6239b3e68618cbbdc9cf18f6afa8cbdda483ec30 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 17:48:49 +0100 Subject: [PATCH 180/679] Adding some TODOs --- .../notification/CanTogglePushNotificationsViaPusherUseCase.kt | 1 + .../CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt | 1 + .../CheckIfCanTogglePushNotificationsViaPusherUseCase.kt | 1 + .../devices/v2/notification/GetNotificationsStatusUseCase.kt | 1 + .../devices/v2/notification/TogglePushNotificationUseCase.kt | 1 + 5 files changed, 5 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt index 963768ca04..af15e2f349 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -25,6 +25,7 @@ import javax.inject.Inject class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { + // TODO add unit tests fun execute(session: Session): Flow { return session .homeServerCapabilitiesService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt index 194a2aebbf..85abd7cd35 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -22,6 +22,7 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { + // TODO update unit tests fun execute(session: Session, deviceId: String): Boolean { return session .accountDataService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt index ca314bf145..9c2a471120 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -21,6 +21,7 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { + // TODO update unit tests fun execute(session: Session): Boolean { return session .homeServerCapabilitiesService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 03e4e31f2e..ed1d54e118 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -34,6 +34,7 @@ class GetNotificationsStatusUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { + // TODO update unit tests fun execute(session: Session, deviceId: String): Flow { return when { checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 7969bbbe9b..28018f9687 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -28,6 +28,7 @@ class TogglePushNotificationUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { + // TODO update unit tests suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return From 18929324fee53d6fc81a4c252a65534c073c25b4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 10:28:24 +0100 Subject: [PATCH 181/679] Updating existing unit tests --- .../vector/app/core/di/ActiveSessionHolder.kt | 1 - .../ConfigureAndStartSessionUseCase.kt | 1 - ...ePushNotificationsViaAccountDataUseCase.kt | 1 - ...TogglePushNotificationsViaPusherUseCase.kt | 1 - .../GetNotificationsStatusUseCase.kt | 1 - .../TogglePushNotificationUseCase.kt | 1 - .../app/core/pushers/PushersManagerTest.kt | 16 ------ .../ConfigureAndStartSessionUseCaseTest.kt | 6 +++ ...hNotificationsViaAccountDataUseCaseTest.kt | 18 +++---- ...lePushNotificationsViaPusherUseCaseTest.kt | 25 ++------- .../GetNotificationsStatusUseCaseTest.kt | 51 +++++++------------ .../TogglePushNotificationUseCaseTest.kt | 18 ++++--- .../overview/SessionOverviewViewModelTest.kt | 9 ++-- .../FakeEnableNotificationsSettingUpdater.kt | 31 +++++++++++ 14 files changed, 82 insertions(+), 98 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 1e9f080303..7e4f73e7a5 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -19,7 +19,6 @@ package im.vector.app.core.di import android.content.Context import im.vector.app.ActiveSessionDataSource import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.session.ConfigureAndStartSessionUseCase diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index 00dc1ab5f9..71863b8642 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -36,7 +36,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, ) { - // TODO update unit tests suspend fun execute(session: Session, startSyncing: Boolean = true) { Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") session.open() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt index 85abd7cd35..194a2aebbf 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt @@ -22,7 +22,6 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { - // TODO update unit tests fun execute(session: Session, deviceId: String): Boolean { return session .accountDataService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt index 9c2a471120..ca314bf145 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt @@ -21,7 +21,6 @@ import javax.inject.Inject class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { - // TODO update unit tests fun execute(session: Session): Boolean { return session .homeServerCapabilitiesService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index ed1d54e118..03e4e31f2e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -34,7 +34,6 @@ class GetNotificationsStatusUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - // TODO update unit tests fun execute(session: Session, deviceId: String): Flow { return when { checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 28018f9687..7969bbbe9b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -28,7 +28,6 @@ class TogglePushNotificationUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - // TODO update unit tests suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt index 113a810ac2..7a1833e057 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt @@ -29,7 +29,6 @@ import im.vector.app.test.fixtures.CryptoDeviceInfoFixture.aCryptoDeviceInfo import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.fixtures.SessionParamsFixture import io.mockk.mockk -import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo @@ -101,19 +100,4 @@ class PushersManagerTest { pusher shouldBeEqualTo expectedPusher } - - @Test - fun `when togglePusherForCurrentSession, then do service toggle pusher`() = runTest { - val deviceId = "device_id" - val sessionParams = SessionParamsFixture.aSessionParams( - credentials = CredentialsFixture.aCredentials(deviceId = deviceId) - ) - session.givenSessionParams(sessionParams) - val pusher = PusherFixture.aPusher(deviceId = deviceId) - pushersService.givenGetPushers(listOf(pusher)) - - pushersManager.togglePusherForCurrentSession(true) - - pushersService.verifyTogglePusherCalled(pusher, true) - } } diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt index 8d4507e85d..861e59e0f1 100644 --- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -19,6 +19,7 @@ package im.vector.app.core.session import im.vector.app.core.extensions.startSyncing import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeWebRtcCallManager @@ -43,12 +44,14 @@ class ConfigureAndStartSessionUseCaseTest { private val fakeWebRtcCallManager = FakeWebRtcCallManager() private val fakeUpdateMatrixClientInfoUseCase = mockk() private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater() private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase( context = fakeContext.instance, webRtcCallManager = fakeWebRtcCallManager.instance, updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase, vectorPreferences = fakeVectorPreferences.instance, + enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance, ) @Before @@ -68,6 +71,7 @@ class ConfigureAndStartSessionUseCaseTest { fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) @@ -87,6 +91,7 @@ class ConfigureAndStartSessionUseCaseTest { fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) @@ -106,6 +111,7 @@ class ConfigureAndStartSessionUseCaseTest { fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) + fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt index 0303444605..37433364e8 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeSession import io.mockk.mockk import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -26,18 +26,15 @@ private const val A_DEVICE_ID = "device-id" class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeSession = FakeSession() private val checkIfCanTogglePushNotificationsViaAccountDataUseCase = - CheckIfCanTogglePushNotificationsViaAccountDataUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - ) + CheckIfCanTogglePushNotificationsViaAccountDataUseCase() @Test fun `given current session and an account data for the device id when execute then result is true`() { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .accountDataService() .givenGetUserAccountDataEventReturns( type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, @@ -45,7 +42,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { ) // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) // Then result shouldBeEqualTo true @@ -54,8 +51,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { @Test fun `given current session and NO account data for the device id when execute then result is false`() { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .accountDataService() .givenGetUserAccountDataEventReturns( type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, @@ -63,7 +59,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { ) // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) // Then result shouldBeEqualTo false diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt index 51874be1bc..508a05acd6 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.settings.devices.v2.notification -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fixtures.aHomeServerCapabilities import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -25,37 +25,22 @@ private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyToggl class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeSession = FakeSession() private val checkIfCanTogglePushNotificationsViaPusherUseCase = - CheckIfCanTogglePushNotificationsViaPusherUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - ) + CheckIfCanTogglePushNotificationsViaPusherUseCase() @Test fun `given current session when execute then toggle capability is returned`() { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .fakeHomeServerCapabilitiesService .givenCapabilities(A_HOMESERVER_CAPABILITIES) // When - val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) // Then result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices } - - @Test - fun `given no current session when execute then false is returned`() { - // Given - fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) - - // When - val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() - - // Then - result shouldBeEqualTo false - } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index b13018a20d..b38367b098 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -17,7 +17,7 @@ package im.vector.app.features.settings.devices.v2.notification import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.testDispatcher import io.mockk.every @@ -25,6 +25,7 @@ import io.mockk.mockk import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain @@ -44,17 +45,16 @@ class GetNotificationsStatusUseCaseTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() - private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = - mockk() + private val fakeSession = FakeSession() private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = mockk() + private val fakeCanTogglePushNotificationsViaPusherUseCase = + mockk() private val getNotificationsStatusUseCase = GetNotificationsStatusUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase, ) @Before @@ -67,33 +67,21 @@ class GetNotificationsStatusUseCaseTest { Dispatchers.resetMain() } - @Test - fun `given NO current session when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { - // Given - fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) - - // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) - - // Then - result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED - } - @Test fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { // Given - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED verifyOrder { // we should first check account data - fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) - fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() + fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } } @@ -106,12 +94,12 @@ class GetNotificationsStatusUseCaseTest { enabled = true, ) ) - fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + fakeSession.pushersService().givenPushersLive(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false + every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED @@ -120,8 +108,7 @@ class GetNotificationsStatusUseCaseTest { @Test fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { // Given - fakeActiveSessionHolder - .fakeSession + fakeSession .accountDataService() .givenGetUserAccountDataEventReturns( type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, @@ -129,11 +116,11 @@ class GetNotificationsStatusUseCaseTest { isSilenced = false ).toContent(), ) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true + every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When - val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt index 0a649354f9..35c5979e53 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt @@ -49,10 +49,11 @@ class TogglePushNotificationUseCaseTest { PusherFixture.aPusher(deviceId = sessionId, enabled = false), PusherFixture.aPusher(deviceId = "another id", enabled = false) ) - activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) - activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns false + val fakeSession = activeSessionHolder.fakeSession + fakeSession.pushersService().givenPushersLive(pushers) + fakeSession.pushersService().givenGetPushers(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false // When togglePushNotificationUseCase.execute(sessionId, true) @@ -69,13 +70,14 @@ class TogglePushNotificationUseCaseTest { PusherFixture.aPusher(deviceId = sessionId, enabled = false), PusherFixture.aPusher(deviceId = "another id", enabled = false) ) - activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) - activeSessionHolder.fakeSession.accountDataService().givenGetUserAccountDataEventReturns( + val fakeSession = activeSessionHolder.fakeSession + fakeSession.pushersService().givenPushersLive(pushers) + fakeSession.accountDataService().givenGetUserAccountDataEventReturns( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = true).toContent() ) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns true + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true // When togglePushNotificationUseCase.execute(sessionId, true) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index f26c818e1d..444e14ec0e 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -37,6 +37,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.just +import io.mockk.justRun +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs @@ -98,7 +100,7 @@ class SessionOverviewViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() - every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) + every { fakeGetNotificationsStatusUseCase.execute(fakeActiveSessionHolder.fakeSession, A_SESSION_ID_1) } returns flowOf(notificationsStatus) } private fun givenVerificationService(): FakeVerificationService { @@ -412,13 +414,10 @@ class SessionOverviewViewModelTest { @Test fun `when viewModel init, then observe pushers and emit to state`() { - val notificationStatus = NotificationsStatus.ENABLED - every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationStatus) - val viewModel = createViewModel() viewModel.test() - .assertLatestState { state -> state.notificationsStatus == notificationStatus } + .assertLatestState { state -> state.notificationsStatus == notificationsStatus } .finish() } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt new file mode 100644 index 0000000000..a78dd1a34b --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.core.notification.EnableNotificationsSettingUpdater +import io.mockk.justRun +import io.mockk.mockk +import org.matrix.android.sdk.api.session.Session + +class FakeEnableNotificationsSettingUpdater { + + val instance = mockk() + + fun givenOnSessionsStarted(session: Session) { + justRun { instance.onSessionsStarted(session) } + } +} From e5e971683b6faa9a85049080f743177b944cfe67 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 11:47:23 +0100 Subject: [PATCH 182/679] Adding unit tests on CanTogglePushNotificationsViaPusherUseCase --- ...TogglePushNotificationsViaPusherUseCase.kt | 1 - ...lePushNotificationsViaPusherUseCaseTest.kt | 65 +++++++++++++++++++ .../FakeHomeServerCapabilitiesService.kt | 10 +++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt index af15e2f349..963768ca04 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -25,7 +25,6 @@ import javax.inject.Inject class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { - // TODO add unit tests fun execute(session: Session): Flow { return session .homeServerCapabilitiesService() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt new file mode 100644 index 0000000000..997fa827f5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeFlowLiveDataConversions +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.givenAsFlow +import im.vector.app.test.fixtures.aHomeServerCapabilities +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test + +private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) + +class CanTogglePushNotificationsViaPusherUseCaseTest { + + private val fakeSession = FakeSession() + private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() + + private val canTogglePushNotificationsViaPusherUseCase = + CanTogglePushNotificationsViaPusherUseCase() + + @Before + fun setUp() { + fakeFlowLiveDataConversions.setup() + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given current session when execute then flow of the toggle capability is returned`() = runTest { + // Given + fakeSession + .fakeHomeServerCapabilitiesService + .givenCapabilitiesLiveReturns(A_HOMESERVER_CAPABILITIES) + .givenAsFlow() + + // When + val result = canTogglePushNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull() + + // Then + result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt index 006789f62b..c816c51c0f 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeServerCapabilitiesService.kt @@ -16,14 +16,24 @@ package im.vector.app.test.fakes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional class FakeHomeServerCapabilitiesService : HomeServerCapabilitiesService by mockk() { fun givenCapabilities(homeServerCapabilities: HomeServerCapabilities) { every { getHomeServerCapabilities() } returns homeServerCapabilities } + + fun givenCapabilitiesLiveReturns(homeServerCapabilities: HomeServerCapabilities): LiveData> { + return MutableLiveData(homeServerCapabilities.toOptional()).also { + every { getHomeServerCapabilitiesLive() } returns it + } + } } From 2eeb04426b49ab5496e5e5fce8a493795fd8505c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 14:22:50 +0100 Subject: [PATCH 183/679] Adding unit tests on DisableNotificationsForCurrentSessionUseCase --- ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...tificationsForCurrentSessionUseCaseTest.kt | 78 +++++++++++++++++++ .../app/test/fakes/FakePushersManager.kt | 25 ++++++ .../app/test/fakes/FakeUnifiedPushHelper.kt | 36 +++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index d66bf8b789..61c884f0bc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -31,7 +31,6 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor( private val togglePushNotificationUseCase: TogglePushNotificationUseCase, ) { - // TODO add unit tests suspend fun execute() { val session = activeSessionHolder.getSafeActiveSession() ?: return val deviceId = session.sessionParams.deviceId ?: return diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt new file mode 100644 index 0000000000..e460413a39 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_SESSION_ID = "session-id" + +class DisableNotificationsForCurrentSessionUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakePushersManager = FakePushersManager() + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() + private val fakeTogglePushNotificationUseCase = mockk() + + private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + pushersManager = fakePushersManager.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + ) + + @Test + fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest { + // Given + val fakeSession = fakeActiveSessionHolder.fakeSession + fakeSession.givenSessionId(A_SESSION_ID) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true + coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + + // When + disableNotificationsForCurrentSessionUseCase.execute() + + // Then + coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) } + } + + @Test + fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest { + // Given + val fakeSession = fakeActiveSessionHolder.fakeSession + fakeSession.givenSessionId(A_SESSION_ID) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false + fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance) + + // When + disableNotificationsForCurrentSessionUseCase.execute() + + // Then + fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt new file mode 100644 index 0000000000..245662b0c6 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.core.pushers.PushersManager +import io.mockk.mockk + +class FakePushersManager { + + val instance = mockk() +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt new file mode 100644 index 0000000000..d7985d9757 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.UnifiedPushHelper +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk + +class FakeUnifiedPushHelper { + + val instance = mockk() + + fun givenUnregister(pushersManager: PushersManager) { + coJustRun { instance.unregister(pushersManager) } + } + + fun verifyUnregister(pushersManager: PushersManager) { + coVerify { instance.unregister(pushersManager) } + } +} From b43c3a8502e6a18df0f2198cbb9ebacb3f487ac6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 14:42:52 +0100 Subject: [PATCH 184/679] Adding unit tests on UpdateEnableNotificationsSettingOnChangeUseCase --- .../EnableNotificationsSettingUpdater.kt | 2 - ...ableNotificationsSettingOnChangeUseCase.kt | 11 ++- ...NotificationsSettingOnChangeUseCaseTest.kt | 90 +++++++++++++++++++ .../overview/SessionOverviewViewModelTest.kt | 12 ++- .../FakeGetNotificationsStatusUseCase.kt | 37 ++++++++ .../app/test/fakes/FakeVectorPreferences.kt | 11 ++- 6 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt index 21febaee9d..81b524cde9 100644 --- a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt +++ b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt @@ -18,7 +18,6 @@ package im.vector.app.core.notification import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import javax.inject.Inject @@ -35,7 +34,6 @@ class EnableNotificationsSettingUpdater @Inject constructor( job?.cancel() job = session.coroutineScope.launch { updateEnableNotificationsSettingOnChangeUseCase.execute(session) - .launchIn(this) } } } diff --git a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt index a6bcf17b5c..55a2cfdc64 100644 --- a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt @@ -19,8 +19,7 @@ package im.vector.app.core.notification import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -35,11 +34,11 @@ class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor( private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, ) { - // TODO add unit tests - fun execute(session: Session): Flow { - val deviceId = session.sessionParams.deviceId ?: return emptyFlow() - return getNotificationsStatusUseCase.execute(session, deviceId) + suspend fun execute(session: Session) { + val deviceId = session.sessionParams.deviceId ?: return + getNotificationsStatusUseCase.execute(session, deviceId) .onEach(::updatePreference) + .collect() } private fun updatePreference(notificationStatus: NotificationsStatus) { diff --git a/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt new file mode 100644 index 0000000000..5cced75735 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCaseTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.notification + +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeVectorPreferences +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_SESSION_ID = "session-id" + +class UpdateEnableNotificationsSettingOnChangeUseCaseTest { + + private val fakeSession = FakeSession().also { it.givenSessionId(A_SESSION_ID) } + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() + + private val updateEnableNotificationsSettingOnChangeUseCase = UpdateEnableNotificationsSettingOnChangeUseCase( + vectorPreferences = fakeVectorPreferences.instance, + getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, + ) + + @Test + fun `given notifications are enabled when execute then setting is updated to true`() = runTest { + // Given + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeSession, + A_SESSION_ID, + NotificationsStatus.ENABLED, + ) + fakeVectorPreferences.givenSetNotificationEnabledForDevice() + + // When + updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession) + + // Then + fakeVectorPreferences.verifySetNotificationEnabledForDevice(true) + } + + @Test + fun `given notifications are disabled when execute then setting is updated to false`() = runTest { + // Given + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeSession, + A_SESSION_ID, + NotificationsStatus.DISABLED, + ) + fakeVectorPreferences.givenSetNotificationEnabledForDevice() + + // When + updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession) + + // Then + fakeVectorPreferences.verifySetNotificationEnabledForDevice(false) + } + + @Test + fun `given notifications toggle is not supported when execute then nothing is done`() = runTest { + // Given + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeSession, + A_SESSION_ID, + NotificationsStatus.NOT_SUPPORTED, + ) + fakeVectorPreferences.givenSetNotificationEnabledForDevice() + + // When + updateEnableNotificationsSettingOnChangeUseCase.execute(fakeSession) + + // Then + fakeVectorPreferences.verifySetNotificationEnabledForDevice(true, inverse = true) + fakeVectorPreferences.verifySetNotificationEnabledForDevice(false, inverse = true) + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 444e14ec0e..86a9969a6a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -22,11 +22,11 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase -import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase @@ -77,7 +77,7 @@ class SessionOverviewViewModelTest { private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() - private val fakeGetNotificationsStatusUseCase = mockk() + private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() private val notificationsStatus = NotificationsStatus.ENABLED private fun createViewModel() = SessionOverviewViewModel( @@ -90,7 +90,7 @@ class SessionOverviewViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, - getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase, + getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, ) @Before @@ -100,7 +100,11 @@ class SessionOverviewViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() - every { fakeGetNotificationsStatusUseCase.execute(fakeActiveSessionHolder.fakeSession, A_SESSION_ID_1) } returns flowOf(notificationsStatus) + fakeGetNotificationsStatusUseCase.givenExecuteReturns( + fakeActiveSessionHolder.fakeSession, + A_SESSION_ID_1, + notificationsStatus + ) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt new file mode 100644 index 0000000000..a9c1b37d69 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeGetNotificationsStatusUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import org.matrix.android.sdk.api.session.Session + +class FakeGetNotificationsStatusUseCase { + + val instance = mockk() + + fun givenExecuteReturns( + session: Session, + sessionId: String, + notificationsStatus: NotificationsStatus + ) { + every { instance.execute(session, sessionId) } returns flowOf(notificationsStatus) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index cd4f70bf63..4baa7e2b90 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -18,6 +18,7 @@ package im.vector.app.test.fakes import im.vector.app.features.settings.VectorPreferences import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.verify @@ -42,5 +43,13 @@ class FakeVectorPreferences { } fun givenTextFormatting(isEnabled: Boolean) = - every { instance.isTextFormattingEnabled() } returns isEnabled + every { instance.isTextFormattingEnabled() } returns isEnabled + + fun givenSetNotificationEnabledForDevice() { + justRun { instance.setNotificationEnabledForDevice(any()) } + } + + fun verifySetNotificationEnabledForDevice(enabled: Boolean, inverse: Boolean = false) { + verify(inverse = inverse) { instance.setNotificationEnabledForDevice(enabled) } + } } From ced4bf3573a51b13ac5211980415978c686ed770 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 4 Nov 2022 15:10:19 +0100 Subject: [PATCH 185/679] Adding unit tests on EnableNotificationsForCurrentSessionUseCase --- ...leNotificationsForCurrentSessionUseCase.kt | 1 - ...tificationsForCurrentSessionUseCaseTest.kt | 87 +++++++++++++++++++ .../im/vector/app/test/fakes/FakeFcmHelper.kt | 44 ++++++++++ .../app/test/fakes/FakePushersManager.kt | 6 ++ .../app/test/fakes/FakeUnifiedPushHelper.kt | 17 ++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index cee653380a..180627a15f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -37,7 +37,6 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor( private val togglePushNotificationUseCase: TogglePushNotificationUseCase, ) { - // TODO add unit tests suspend fun execute(fragmentActivity: FragmentActivity) { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt new file mode 100644 index 0000000000..eb6629cb13 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.notifications + +import androidx.fragment.app.FragmentActivity +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeFcmHelper +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_SESSION_ID = "session-id" + +class EnableNotificationsForCurrentSessionUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakePushersManager = FakePushersManager() + private val fakeFcmHelper = FakeFcmHelper() + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() + private val fakeTogglePushNotificationUseCase = mockk() + + private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + pushersManager = fakePushersManager.instance, + fcmHelper = fakeFcmHelper.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + ) + + @Test + fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest { + // Given + val fragmentActivity = mockk() + fakePushersManager.givenGetPusherForCurrentSessionReturns(null) + fakeUnifiedPushHelper.givenRegister(fragmentActivity) + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false + + // When + enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + + // Then + fakeUnifiedPushHelper.verifyRegister(fragmentActivity) + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true) + } + + @Test + fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest { + // Given + val fragmentActivity = mockk() + fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk()) + val fakeSession = fakeActiveSessionHolder.fakeSession + fakeSession.givenSessionId(A_SESSION_ID) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true + coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + + // When + enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + + // Then + coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt new file mode 100644 index 0000000000..11abf18794 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import androidx.fragment.app.FragmentActivity +import im.vector.app.core.pushers.FcmHelper +import im.vector.app.core.pushers.PushersManager +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify + +class FakeFcmHelper { + + val instance = mockk() + + fun givenEnsureFcmTokenIsRetrieved( + fragmentActivity: FragmentActivity, + pushersManager: PushersManager, + ) { + justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) } + } + + fun verifyEnsureFcmTokenIsRetrieved( + fragmentActivity: FragmentActivity, + pushersManager: PushersManager, + registerPusher: Boolean, + ) { + verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt index 245662b0c6..46d852f4f8 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt @@ -17,9 +17,15 @@ package im.vector.app.test.fakes import im.vector.app.core.pushers.PushersManager +import io.mockk.every import io.mockk.mockk +import org.matrix.android.sdk.api.session.pushers.Pusher class FakePushersManager { val instance = mockk() + + fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) { + every { instance.getPusherForCurrentSession() } returns pusher + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt index d7985d9757..1f2cc8a1ce 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -16,16 +16,29 @@ package im.vector.app.test.fakes +import androidx.fragment.app.FragmentActivity import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper import io.mockk.coJustRun import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk +import io.mockk.verify class FakeUnifiedPushHelper { val instance = mockk() + fun givenRegister(fragmentActivity: FragmentActivity) { + every { instance.register(fragmentActivity, any()) } answers { + secondArg().run() + } + } + + fun verifyRegister(fragmentActivity: FragmentActivity) { + verify { instance.register(fragmentActivity, any()) } + } + fun givenUnregister(pushersManager: PushersManager) { coJustRun { instance.unregister(pushersManager) } } @@ -33,4 +46,8 @@ class FakeUnifiedPushHelper { fun verifyUnregister(pushersManager: PushersManager) { coVerify { instance.unregister(pushersManager) } } + + fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) { + every { instance.isEmbeddedDistributor() } returns isEmbedded + } } From 163bf57fdaf001bd7f8366b850acad8e43fb0f4b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 8 Nov 2022 17:14:57 +0100 Subject: [PATCH 186/679] Removing non necessary debug log --- .../UpdateEnableNotificationsSettingOnChangeUseCase.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt index 55a2cfdc64..36df939bad 100644 --- a/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/notification/UpdateEnableNotificationsSettingOnChangeUseCase.kt @@ -22,7 +22,6 @@ import im.vector.app.features.settings.devices.v2.notification.NotificationsStat import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.Session -import timber.log.Timber import javax.inject.Inject /** @@ -42,7 +41,6 @@ class UpdateEnableNotificationsSettingOnChangeUseCase @Inject constructor( } private fun updatePreference(notificationStatus: NotificationsStatus) { - Timber.d("updatePreference with status=$notificationStatus") when (notificationStatus) { NotificationsStatus.ENABLED -> vectorPreferences.setNotificationEnabledForDevice(true) NotificationsStatus.DISABLED -> vectorPreferences.setNotificationEnabledForDevice(false) From ba5a433cafc8b6ed6cdace2a526bead13c08bf34 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 8 Nov 2022 17:19:06 +0100 Subject: [PATCH 187/679] Adding distinctUntilChanged for flow of remote toggle via Pusher capability --- .../notification/CanTogglePushNotificationsViaPusherUseCase.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt index 963768ca04..0125d92ba6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt @@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2.notification import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.flow.unwrap @@ -32,5 +33,6 @@ class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { .asFlow() .unwrap() .map { it.canRemotelyTogglePushNotificationsOfDevices } + .distinctUntilChanged() } } From 6ec33f1264ecc59b26a8db8844dafacaa9f83a18 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 9 Nov 2022 09:33:39 +0100 Subject: [PATCH 188/679] Removing unused imports --- .../devices/v2/overview/SessionOverviewViewModelTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 86a9969a6a..1a57b76020 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -37,8 +37,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.just -import io.mockk.justRun -import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs From def74926d771256b05149ffcdc1b9074c25f9996 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 9 Nov 2022 11:40:44 +0100 Subject: [PATCH 189/679] Adding changelog entry --- changelog.d/7555.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7555.bugfix diff --git a/changelog.d/7555.bugfix b/changelog.d/7555.bugfix new file mode 100644 index 0000000000..064b21a9e5 --- /dev/null +++ b/changelog.d/7555.bugfix @@ -0,0 +1 @@ +Missing translations on "replyTo" messages From ab90da0e51e35b2d6d3d0eb93ce8000e0854fa2e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 9 Nov 2022 17:30:46 +0100 Subject: [PATCH 190/679] Adding isReply extension method for RelationDefaultContent --- .../org/matrix/android/sdk/api/session/events/model/Event.kt | 3 ++- .../api/session/room/model/relation/RelationDefaultContent.kt | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 1f16041b54..57bd3dbc41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageStickerConte import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.model.relation.isReply import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.threads.ThreadDetails @@ -420,7 +421,7 @@ fun Event.getRelationContentForType(type: String): RelationDefaultContent? = getRelationContent()?.takeIf { it.type == type } fun Event.isReply(): Boolean { - return getRelationContent()?.inReplyTo?.eventId != null + return getRelationContent().isReply() } fun Event.isReplyRenderedInThread(): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt index 5dcb1b4323..b9f9335dbd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt @@ -28,3 +28,5 @@ data class RelationDefaultContent( ) : RelationContent fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false + +fun RelationDefaultContent?.isReply(): Boolean = this?.inReplyTo?.eventId != null From 8814117f26c8e6c099464c942f20036d9ad1d39b Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 8 Nov 2022 13:45:46 +0000 Subject: [PATCH 191/679] Translated using Weblate (Persian) Currently translated at 99.6% (2530 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/ --- .../src/main/res/values-fa/strings.xml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index f2701519e7..313734290f 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -2802,4 +2802,27 @@ ۱ گزیده %1$d گزیده + اجازه‌های لازم برای آغاز پخش صوتی در این اتاق را ندارید. برای ارتقای اجازه‌هایتان با یک مدیر اتاق تماس بگیرید. + فرد دیگری در حال ضبط یک پخش صوتی است. برای آغاز یک پخش جدید، منتظر پایان پخشش بمانید. + با بررسی افزاره‌های وارد شده‌تان باید کد زیر را ببینید. تأیید کنید که این کد با آن افزاره مطابق است: + دارید یک پخش صوتی ضبط می‌کنید. لطفاً برای آغاز یک پخش جدید، به پخش کنونی پایان دهید. + ⚠ افزاره‌های تأییدنشده‌ای در این اتاق وجود دارند. آن‌ها قادر به رمزگشایی پیام‌هایی که فرستاده‌اید نیستند. + استفاده از دوربین روی این افزاره برای پویش کد QR نشان داده شده روی افزارهٔ دیگرتان: + ضبط نام کارخواه، نگارش و نشانی برای بازشناسی آسان‌تر نشست‌ها در مدیر نشست. + 🔒 رمزگذاری به نشست‌های تأیید شده را فقط برای تمامی اتاق‌ها در تنظیمات امنیت به کار انداخته‌اید. + + خارج شدن از نشست‌های قدیمی (۱ روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید. + خارج شدن از نشست‌های قدیمی (%1$d روز یا بیش‌تر) که دیگر استفاده نمی‌کنید را در نظر داشته باشید. + + توانایی ضبط و فرستادن پخش صدا در خط زمانی اتاق. + پویش کد QR زیر با افزاره‌ای که خارج شده. + استفاده از افزارهٔ وارد شده‌تان برای پویش کد QR زیر: + چیزی اشتباه پیش رفت. لطفاً اتّصال شبکه‌تان را بررسی و دوباره تلاش کنید. + ${app_name} برای نمایش آگاهی‌ها نیازمند اجازه است. +\nلطفاً اجازه را اعطا کنید. + نمی‌توان پخش صدایی جدید را آغاز کرد + تغییر حالت تمام‌صفحه + ۳۰ ثانیه پیش‌روی + ۳۰ ثانیه پس‌روی + قالب‌بندی متن \ No newline at end of file From 46615082a9801473626868fde5b336a707ced23b Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 9 Nov 2022 03:15:24 +0000 Subject: [PATCH 192/679] Translated using Weblate (Japanese) Currently translated at 88.4% (2246 of 2539 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- .../ui-strings/src/main/res/values-ja/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml index 37c0bca52f..11ab6ee857 100644 --- a/library/ui-strings/src/main/res/values-ja/strings.xml +++ b/library/ui-strings/src/main/res/values-ja/strings.xml @@ -2459,4 +2459,18 @@ ルームを作成 チャットを開始 全ての会話 + ${app_name}にようこそ、 +\n%s。 + 認証済のセッション + QRコードでサインイン + 新しいセッションマネージャーを有効にする + QRコードでサインイン + 3 + 2 + 1 + リクエストが失敗しました。 + QRコードをスキャン + QRコードをスキャン + QRコードをスキャン + QRコードが不正です。 \ No newline at end of file From 218026f5df2bf2bc4247d77408ea7c1d6d0df8df Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 8 Nov 2022 03:38:31 +0000 Subject: [PATCH 193/679] Translated using Weblate (German) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/de/ --- fastlane/metadata/android/de-DE/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/40105070.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/40105070.txt b/fastlane/metadata/android/de-DE/changelogs/40105070.txt new file mode 100644 index 0000000000..3141cea7cb --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Die wichtigste Änderung in dieser Version: Neue Anhangauswahl-Oberfläche. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases From 15f946c62cc8b7a48bbd784649449bc27d8b926c Mon Sep 17 00:00:00 2001 From: Glandos Date: Wed, 9 Nov 2022 09:53:47 +0000 Subject: [PATCH 194/679] Translated using Weblate (French) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/ --- fastlane/metadata/android/fr-FR/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40105070.txt diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105070.txt b/fastlane/metadata/android/fr-FR/changelogs/40105070.txt new file mode 100644 index 0000000000..b33f290d0d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : nouvelle interface de sélection d’une pièce jointe. +Intégralité des changements : https://github.com/vector-im/element-android/releases From 6faec3d9bd9a13434ae9b691c3746dcd78c55e71 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Tue, 8 Nov 2022 04:05:32 +0000 Subject: [PATCH 195/679] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/ --- fastlane/metadata/android/pt-BR/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40105070.txt diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105070.txt b/fastlane/metadata/android/pt-BR/changelogs/40105070.txt new file mode 100644 index 0000000000..108a8a88b4 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: novo UI para selecionar um anexo. +Changelog completo: https://github.com/vector-im/element-android/releases From ba2fbf10e5d02723c87e21df68b56ec9cadf347f Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 8 Nov 2022 10:11:06 +0000 Subject: [PATCH 196/679] Translated using Weblate (Slovak) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/ --- fastlane/metadata/android/sk/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sk/changelogs/40105070.txt diff --git a/fastlane/metadata/android/sk/changelogs/40105070.txt b/fastlane/metadata/android/sk/changelogs/40105070.txt new file mode 100644 index 0000000000..0d1d4965ca --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: nové používateľské rozhranie na výber príloh. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases From ae150a262370a380c7201bde7fe3baba44e6ce02 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 8 Nov 2022 09:17:39 +0000 Subject: [PATCH 197/679] Translated using Weblate (Ukrainian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/ --- fastlane/metadata/android/uk/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/40105070.txt diff --git a/fastlane/metadata/android/uk/changelogs/40105070.txt b/fastlane/metadata/android/uk/changelogs/40105070.txt new file mode 100644 index 0000000000..65254059c5 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: новий інтерфейс для вибору вкладень. +Перелік усіх змін: https://github.com/vector-im/element-android/releases From ab396c5f7fff36d61ff11d8e3c9ed12f092ec0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 8 Nov 2022 07:21:52 +0000 Subject: [PATCH 198/679] Translated using Weblate (Estonian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/et/ --- fastlane/metadata/android/et/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/40105070.txt diff --git a/fastlane/metadata/android/et/changelogs/40105070.txt b/fastlane/metadata/android/et/changelogs/40105070.txt new file mode 100644 index 0000000000..061e09814d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: uus liides manuste valimiseks. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases From 48972a1a18e672d2138d567eda340efb9c4bd2c0 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 8 Nov 2022 13:32:49 +0000 Subject: [PATCH 199/679] Translated using Weblate (Persian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/fa/ --- fastlane/metadata/android/fa/changelogs/40105060.txt | 2 ++ fastlane/metadata/android/fa/changelogs/40105070.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/fa/changelogs/40105060.txt create mode 100644 fastlane/metadata/android/fa/changelogs/40105070.txt diff --git a/fastlane/metadata/android/fa/changelogs/40105060.txt b/fastlane/metadata/android/fa/changelogs/40105060.txt new file mode 100644 index 0000000000..b677c05c89 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105060.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رابط کاربری جدید برای گزینش پیوست. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/fa/changelogs/40105070.txt b/fastlane/metadata/android/fa/changelogs/40105070.txt new file mode 100644 index 0000000000..b677c05c89 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40105070.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: رابط کاربری جدید برای گزینش پیوست. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases From 4d27d568aa66e87850e5669a2021897d29dc2d1e Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 8 Nov 2022 03:32:34 +0000 Subject: [PATCH 200/679] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/ --- fastlane/metadata/android/zh-TW/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-TW/changelogs/40105070.txt diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105070.txt b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt new file mode 100644 index 0000000000..56667ccfc0 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105070.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:選取附件的新使用者介面。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases From 26b16fca187af18c1a0b0291cf95f75520b2822c Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Tue, 8 Nov 2022 06:13:17 +0000 Subject: [PATCH 201/679] Translated using Weblate (Czech) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/ --- fastlane/metadata/android/cs-CZ/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/40105070.txt diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt new file mode 100644 index 0000000000..e966dbbd92 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: nové uživatelské rozhraní pro výběr přílohy. +Úplný seznam změn: https://github.com/vector-im/element-android/releases From f39e3538a1fc2039ad8d5b083a228beafa30ebbe Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 8 Nov 2022 05:58:49 +0000 Subject: [PATCH 202/679] Translated using Weblate (Indonesian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/id/ --- fastlane/metadata/android/id/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/id/changelogs/40105070.txt diff --git a/fastlane/metadata/android/id/changelogs/40105070.txt b/fastlane/metadata/android/id/changelogs/40105070.txt new file mode 100644 index 0000000000..32fb87563e --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Antarmuka baru untuk memilih sebuah lampiran. +Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases From 6783b11a63cfa8f6ab71454ffc1596e94ba7da4d Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 8 Nov 2022 21:10:13 +0000 Subject: [PATCH 203/679] Translated using Weblate (Albanian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/ --- fastlane/metadata/android/sq/changelogs/40105070.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/40105070.txt diff --git a/fastlane/metadata/android/sq/changelogs/40105070.txt b/fastlane/metadata/android/sq/changelogs/40105070.txt new file mode 100644 index 0000000000..f4beb912a5 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë verson: ndërfaqe UI e re për përzgjedhje të një bashkëngjitjeje. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases From c07b110b99c93ab59b8944b688405ee79dc9a95c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 10 Nov 2022 16:13:09 +0530 Subject: [PATCH 204/679] Add spannable tracking around SyncResponseHandler (#7514) * Add spannable tracking around SyncResponseHandler * Update LICENSE header * Refactor handleResponse and MetricsExtensions * Update changelog.d * Improve code docs and comments * Check if Sentry is enabled before tracking --- changelog.d/7514.sdk | 1 + .../sdk/api/extensions/MetricsExtensions.kt | 34 +++- .../sdk/api/metrics/SpannableMetricPlugin.kt | 36 ++++ .../api/metrics/SyncDurationMetricPlugin.kt | 32 ++++ .../sdk/internal/crypto/DeviceListManager.kt | 2 +- .../session/sync/SyncResponseHandler.kt | 169 +++++++++++++----- .../analytics/metrics/VectorPlugins.kt | 4 +- .../sentry/SentryDownloadDeviceKeysMetrics.kt | 6 +- .../sentry/SentrySyncDurationMetrics.kt | 89 +++++++++ 9 files changed, 320 insertions(+), 53 deletions(-) create mode 100644 changelog.d/7514.sdk create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt diff --git a/changelog.d/7514.sdk b/changelog.d/7514.sdk new file mode 100644 index 0000000000..f335156a49 --- /dev/null +++ b/changelog.d/7514.sdk @@ -0,0 +1 @@ +[Metrics] Add `SpannableMetricPlugin` to support spans within transactions. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt index 9487a27086..7f0e828f62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt @@ -17,25 +17,51 @@ package org.matrix.android.sdk.api.extensions import org.matrix.android.sdk.api.metrics.MetricPlugin +import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** * Executes the given [block] while measuring the transaction. + * + * @param block Action/Task to be executed within this span. + */ +@OptIn(ExperimentalContracts::class) +inline fun List.measureMetric(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + try { + this.forEach { plugin -> plugin.startTransaction() } // Start the transaction. + block() + } catch (throwable: Throwable) { + this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown. + throw throwable + } finally { + this.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction. + } +} + +/** + * Executes the given [block] while measuring a span. + * + * @param operation Name of the new span. + * @param description Description of the new span. + * @param block Action/Task to be executed within this span. */ @OptIn(ExperimentalContracts::class) -inline fun measureMetric(metricMeasurementPlugins: List, block: () -> Unit) { +inline fun List.measureSpan(operation: String, description: String, block: () -> Unit) { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } try { - metricMeasurementPlugins.forEach { plugin -> plugin.startTransaction() } // Start the transaction. + this.forEach { plugin -> plugin.startSpan(operation, description) } // Start the transaction. block() } catch (throwable: Throwable) { - metricMeasurementPlugins.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown. + this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown. throw throwable } finally { - metricMeasurementPlugins.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction. + this.forEach { plugin -> plugin.finishSpan() } // Finally, finish this transaction. } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt new file mode 100644 index 0000000000..54aa21877e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.metrics + +/** + * A plugin that tracks span along with transactions. + */ +interface SpannableMetricPlugin : MetricPlugin { + + /** + * Starts the span for a sub-task. + * + * @param operation Name of the new span. + * @param description Description of the new span. + */ + fun startSpan(operation: String, description: String) + + /** + * Finish the span when sub-task is completed. + */ + fun finishSpan() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt new file mode 100644 index 0000000000..79ece002e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.metrics + +import org.matrix.android.sdk.api.logger.LoggerTag +import timber.log.Timber + +private val loggerTag = LoggerTag("SyncDurationMetricPlugin", LoggerTag.CRYPTO) + +/** + * An spannable metric plugin for sync response handling task. + */ +interface SyncDurationMetricPlugin : SpannableMetricPlugin { + + override fun logTransaction(message: String?) { + Timber.tag(loggerTag.value).v("## syncResponseHandler() : $message") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 2ac6b8c854..7e9e156003 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -355,7 +355,7 @@ internal class DeviceListManager @Inject constructor( val relevantPlugins = metricPlugins.filterIsInstance() val response: KeysQueryResponse - measureMetric(relevantPlugins) { + relevantPlugins.measureMetric { response = try { downloadKeysForUsersTask.execute(params) } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 05216d1de1..05d50d9595 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -17,6 +17,11 @@ package org.matrix.android.sdk.internal.session.sync import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.extensions.measureMetric +import org.matrix.android.sdk.api.extensions.measureSpan +import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin import org.matrix.android.sdk.api.session.pushrules.PushRuleService import org.matrix.android.sdk.api.session.pushrules.RuleScope import org.matrix.android.sdk.api.session.sync.InitialSyncStep @@ -52,9 +57,12 @@ internal class SyncResponseHandler @Inject constructor( private val tokenStore: SyncTokenStore, private val processEventForPushTask: ProcessEventForPushTask, private val pushRuleService: PushRuleService, - private val presenceSyncHandler: PresenceSyncHandler + private val presenceSyncHandler: PresenceSyncHandler, + matrixConfiguration: MatrixConfiguration, ) { + private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance() + suspend fun handleResponse( syncResponse: SyncResponse, fromToken: String?, @@ -63,39 +71,91 @@ internal class SyncResponseHandler @Inject constructor( val isInitialSync = fromToken == null Timber.v("Start handling sync, is InitialSync: $isInitialSync") - measureTimeMillis { - if (!cryptoService.isStarted()) { - Timber.v("Should start cryptoService") - cryptoService.start() - } - cryptoService.onSyncWillProcess(isInitialSync) - }.also { - Timber.v("Finish handling start cryptoService in $it ms") + relevantPlugins.measureMetric { + startCryptoService(isInitialSync) + + // Handle the to device events before the room ones + // to ensure to decrypt them properly + handleToDevice(syncResponse, reporter) + + val aggregator = SyncResponsePostTreatmentAggregator() + + // Prerequisite for thread events handling in RoomSyncHandler + // Disabled due to the new fallback + // if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { + // threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) + // } + + startMonarchyTransaction(syncResponse, isInitialSync, reporter, aggregator) + + aggregateSyncResponse(aggregator) + + postTreatmentSyncResponse(syncResponse, isInitialSync) + + markCryptoSyncCompleted(syncResponse) + + handlePostSync() + + Timber.v("On sync completed") } + } - // Handle the to device events before the room ones - // to ensure to decrypt them properly - measureTimeMillis { - Timber.v("Handle toDevice") - reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) { - if (syncResponse.toDevice != null) { - cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + private fun startCryptoService(isInitialSync: Boolean) { + relevantPlugins.measureSpan("task", "start_crypto_service") { + measureTimeMillis { + if (!cryptoService.isStarted()) { + Timber.v("Should start cryptoService") + cryptoService.start() } + cryptoService.onSyncWillProcess(isInitialSync) + }.also { + Timber.v("Finish handling start cryptoService in $it ms") } - }.also { - Timber.v("Finish handling toDevice in $it ms") } - val aggregator = SyncResponsePostTreatmentAggregator() + } - // Prerequisite for thread events handling in RoomSyncHandler -// Disabled due to the new fallback -// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) { -// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) -// } + private suspend fun handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) { + relevantPlugins.measureSpan("task", "handle_to_device") { + measureTimeMillis { + Timber.v("Handle toDevice") + reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) { + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + } + } + }.also { + Timber.v("Finish handling toDevice in $it ms") + } + } + } + private suspend fun startMonarchyTransaction( + syncResponse: SyncResponse, + isInitialSync: Boolean, + reporter: ProgressReporter?, + aggregator: SyncResponsePostTreatmentAggregator + ) { // Start one big transaction - monarchy.awaitTransaction { realm -> - // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local) + relevantPlugins.measureSpan("task", "monarchy_transaction") { + monarchy.awaitTransaction { realm -> + // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local) + handleRooms(reporter, syncResponse, realm, isInitialSync, aggregator) + handleAccountData(reporter, realm, syncResponse) + handlePresence(realm, syncResponse) + + tokenStore.saveToken(realm, syncResponse.nextBatch) + } + } + } + + private fun handleRooms( + reporter: ProgressReporter?, + syncResponse: SyncResponse, + realm: Realm, + isInitialSync: Boolean, + aggregator: SyncResponsePostTreatmentAggregator + ) { + relevantPlugins.measureSpan("task", "handle_rooms") { measureTimeMillis { Timber.v("Handle rooms") reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) { @@ -106,7 +166,11 @@ internal class SyncResponseHandler @Inject constructor( }.also { Timber.v("Finish handling rooms in $it ms") } + } + } + private fun handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "handle_account_data") { measureTimeMillis { reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) { Timber.v("Handle accountData") @@ -115,44 +179,59 @@ internal class SyncResponseHandler @Inject constructor( }.also { Timber.v("Finish handling accountData in $it ms") } + } + } + private fun handlePresence(realm: Realm, syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "handle_presence") { measureTimeMillis { Timber.v("Handle Presence") presenceSyncHandler.handle(realm, syncResponse.presence) }.also { Timber.v("Finish handling Presence in $it ms") } - tokenStore.saveToken(realm, syncResponse.nextBatch) } + } - // Everything else we need to do outside the transaction - measureTimeMillis { - aggregatorHandler.handle(aggregator) - }.also { - Timber.v("Aggregator management took $it ms") + private suspend fun aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) { + relevantPlugins.measureSpan("task", "aggregator_management") { + // Everything else we need to do outside the transaction + measureTimeMillis { + aggregatorHandler.handle(aggregator) + }.also { + Timber.v("Aggregator management took $it ms") + } } + } - measureTimeMillis { - syncResponse.rooms?.let { - checkPushRules(it, isInitialSync) - userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) - dispatchInvitedRoom(it) + private suspend fun postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) { + relevantPlugins.measureSpan("task", "sync_response_post_treatment") { + measureTimeMillis { + syncResponse.rooms?.let { + checkPushRules(it, isInitialSync) + userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) + dispatchInvitedRoom(it) + } + }.also { + Timber.v("SyncResponse.rooms post treatment took $it ms") } - }.also { - Timber.v("SyncResponse.rooms post treatment took $it ms") } + } - measureTimeMillis { - cryptoSyncHandler.onSyncCompleted(syncResponse) - }.also { - Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") + private fun markCryptoSyncCompleted(syncResponse: SyncResponse) { + relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") { + measureTimeMillis { + cryptoSyncHandler.onSyncCompleted(syncResponse) + }.also { + Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") + } } + } - // post sync stuffs + private fun handlePostSync() { monarchy.writeAsync { roomSyncHandler.postSyncSpaceHierarchyHandle(it) } - Timber.v("On sync completed") } private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) { diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt index 64f143a2fd..4278c1011b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt @@ -17,6 +17,7 @@ package im.vector.app.features.analytics.metrics import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics +import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics import org.matrix.android.sdk.api.metrics.MetricPlugin import javax.inject.Inject import javax.inject.Singleton @@ -27,9 +28,10 @@ import javax.inject.Singleton @Singleton data class VectorPlugins @Inject constructor( val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics, + val sentrySyncDurationMetrics: SentrySyncDurationMetrics, ) { /** * Returns [List] of all [MetricPlugin] hold by this class. */ - fun plugins(): List = listOf(sentryDownloadDeviceKeysMetrics) + fun plugins(): List = listOf(sentryDownloadDeviceKeysMetrics, sentrySyncDurationMetrics) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt index 92213d380c..488b72bfd9 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryDownloadDeviceKeysMetrics.kt @@ -26,8 +26,10 @@ class SentryDownloadDeviceKeysMetrics @Inject constructor() : DownloadDeviceKeys private var transaction: ITransaction? = null override fun startTransaction() { - transaction = Sentry.startTransaction("download_device_keys", "task") - logTransaction("Sentry transaction started") + if (Sentry.isEnabled()) { + transaction = Sentry.startTransaction("download_device_keys", "task") + logTransaction("Sentry transaction started") + } } override fun finishTransaction() { diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt new file mode 100644 index 0000000000..d69ed01526 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentrySyncDurationMetrics.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.metrics.sentry + +import io.sentry.ISpan +import io.sentry.ITransaction +import io.sentry.Sentry +import io.sentry.SpanStatus +import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin +import java.util.EmptyStackException +import java.util.Stack +import javax.inject.Inject + +/** + * Sentry based implementation of SyncDurationMetricPlugin. + */ +class SentrySyncDurationMetrics @Inject constructor() : SyncDurationMetricPlugin { + private var transaction: ITransaction? = null + + // Stacks to keep spans in LIFO order. + private var spans: Stack = Stack() + + /** + * Starts the span for a sub-task. + * + * @param operation Name of the new span. + * @param description Description of the new span. + * + * @throws IllegalStateException if this is called without starting a transaction ie. `measureSpan` must be called within `measureMetric`. + */ + override fun startSpan(operation: String, description: String) { + if (Sentry.isEnabled()) { + val span = Sentry.getSpan() ?: throw IllegalStateException("measureSpan block must be called within measureMetric") + val innerSpan = span.startChild(operation, description) + spans.push(innerSpan) + logTransaction("Sentry span started: operation=[$operation], description=[$description]") + } + } + + override fun finishSpan() { + try { + spans.pop() + } catch (e: EmptyStackException) { + null + }?.finish() + logTransaction("Sentry span finished") + } + + override fun startTransaction() { + if (Sentry.isEnabled()) { + transaction = Sentry.startTransaction("sync_response_handler", "task", true) + logTransaction("Sentry transaction started") + } + } + + override fun finishTransaction() { + transaction?.finish() + logTransaction("Sentry transaction finished") + } + + override fun onError(throwable: Throwable) { + try { + spans.peek() + } catch (e: EmptyStackException) { + null + }?.apply { + this.throwable = throwable + this.status = SpanStatus.INTERNAL_ERROR + } ?: transaction?.apply { + this.throwable = throwable + this.status = SpanStatus.INTERNAL_ERROR + } + logTransaction("Sentry transaction encountered error ${throwable.message}") + } +} From 235b629130ee5ec93e163fd42ac61918a6547c35 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 9 Nov 2022 17:31:00 +0100 Subject: [PATCH 205/679] Use case to process formatted body of reply to events --- .../src/main/res/values/strings.xml | 9 ++ .../sdk/api/session/events/model/Event.kt | 7 +- .../timeline/factory/MessageItemFactory.kt | 32 ++++- .../timeline/render/EventTextRenderer.kt | 18 +-- .../ProcessBodyOfReplyToEventUseCase.kt | 128 ++++++++++++++++++ 5 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f05a2a11e6..eccd40b2a9 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3458,4 +3458,13 @@ Apply underline format Toggle full screen mode + + In reply to + sent a file. + sent an audio file. + sent a voice message. + sent an image. + sent a video. + sent a sticker. + created a poll. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 57bd3dbc41..1c09b49298 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -229,11 +229,14 @@ data class Event( return when { isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) isFileMessage() -> "sent a file." + isVoiceMessage() -> "sent a voice message." isAudioMessage() -> "sent an audio file." isImageMessage() -> "sent an image." isVideoMessage() -> "sent a video." - isSticker() -> "sent a sticker" + isSticker() -> "sent a sticker." isPoll() -> getPollQuestion() ?: "created a poll." + isLiveLocation() -> "Live location." + isLocationMessage() -> "has shared their location." else -> text } } @@ -444,7 +447,7 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && content?.toModel()?.membership == Membership.INVITE fun Event.getPollContent(): MessagePollContent? { - return content.toModel() + return getDecryptedContent().toModel() } fun Event.supportsNotification() = diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f4d506fa4b..373410775b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -65,6 +65,7 @@ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_ import im.vector.app.features.home.room.detail.timeline.render.EventTextRenderer +import im.vector.app.features.home.room.detail.timeline.render.ProcessBodyOfReplyToEventUseCase import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.html.EventHtmlRenderer @@ -106,6 +107,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl +import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.util.MimeTypes import javax.inject.Inject @@ -139,6 +141,7 @@ class MessageItemFactory @Inject constructor( private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, private val pollItemViewStateFactory: PollItemViewStateFactory, private val voiceBroadcastItemFactory: VoiceBroadcastItemFactory, + private val processBodyOfReplyToEventUseCase: ProcessBodyOfReplyToEventUseCase, ) { // TODO inject this properly? @@ -200,7 +203,7 @@ class MessageItemFactory @Inject constructor( is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) - is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) + is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(event, highlight, attributes) is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } @@ -437,7 +440,14 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes ): MessageTextItem? { // For compatibility reason we should display the body - return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) + return buildMessageTextItem( + messageContent.body, + false, + informationData, + highlight, + callback, + attributes, + ) } private fun buildImageMessageItem( @@ -540,7 +550,8 @@ class MessageItemFactory @Inject constructor( ): VectorEpoxyModel<*>? { val matrixFormattedBody = messageContent.matrixFormattedBody return if (matrixFormattedBody != null) { - buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) + val replyToContent = messageContent.relatesTo?.inReplyTo + buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes, replyToContent) } else { buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } @@ -552,10 +563,21 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, + replyToContent: ReplyToContent?, ): MessageTextItem? { - val compressed = htmlCompressor.compress(matrixFormattedBody) + val processedBody = replyToContent + ?.let { processBodyOfReplyToEventUseCase.execute(roomId, matrixFormattedBody, it) } + ?: matrixFormattedBody + val compressed = htmlCompressor.compress(processedBody) val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned - return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) + return buildMessageTextItem( + renderedFormattedBody, + true, + informationData, + highlight, + callback, + attributes, + ) } private fun buildMessageTextItem( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt index 920f3e3b80..c46112f995 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt @@ -34,22 +34,22 @@ class EventTextRenderer @AssistedInject constructor( @Assisted private val roomId: String?, private val context: Context, private val avatarRenderer: AvatarRenderer, - private val sessionHolder: ActiveSessionHolder + private val activeSessionHolder: ActiveSessionHolder, ) { - /* ========================================================================================== - * Public api - * ========================================================================================== */ - @AssistedFactory interface Factory { fun create(roomId: String?): EventTextRenderer } /** - * @param text the text you want to render + * @param text the text to be rendered */ fun render(text: CharSequence): CharSequence { + return renderNotifyEveryone(text) + } + + private fun renderNotifyEveryone(text: CharSequence): CharSequence { return if (roomId != null && text.contains(MatrixItem.NOTIFY_EVERYONE)) { SpannableStringBuilder(text).apply { addNotifyEveryoneSpans(this, roomId) @@ -59,12 +59,8 @@ class EventTextRenderer @AssistedInject constructor( } } - /* ========================================================================================== - * Helper methods - * ========================================================================================== */ - private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) { - val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId) + val room: RoomSummary? = activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId) val matrixItem = MatrixItem.EveryoneInRoomItem( id = roomId, avatarUrl = room?.avatarUrl, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt new file mode 100644 index 0000000000..44fd5a397c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.render + +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.session.events.model.getPollQuestion +import org.matrix.android.sdk.api.session.events.model.isAudioMessage +import org.matrix.android.sdk.api.session.events.model.isFileMessage +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isLiveLocation +import org.matrix.android.sdk.api.session.events.model.isPoll +import org.matrix.android.sdk.api.session.events.model.isSticker +import org.matrix.android.sdk.api.session.events.model.isVideoMessage +import org.matrix.android.sdk.api.session.events.model.isVoiceMessage +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent +import javax.inject.Inject + +private const val IN_REPLY_TO = "In reply to" +private const val BREAKING_LINE = "
" +private const val ENDING_BLOCK_QUOTE = "" + +// TODO add unit tests +class ProcessBodyOfReplyToEventUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider, +) { + + fun execute(roomId: String, matrixFormattedBody: String, replyToContent: ReplyToContent): String { + val repliedToEvent = replyToContent.eventId?.let { getEvent(it, roomId) } + val breakingLineIndex = matrixFormattedBody.lastIndexOf(BREAKING_LINE) + val endOfBlockQuoteIndex = matrixFormattedBody.lastIndexOf(ENDING_BLOCK_QUOTE) + + // TODO check in other platform how is handled the case of no repliedToEvent fetched + val withTranslatedContent = if (repliedToEvent != null && breakingLineIndex != -1 && endOfBlockQuoteIndex != -1) { + val afterBreakingLineIndex = breakingLineIndex + BREAKING_LINE.length + when { + repliedToEvent.isFileMessage() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + stringProvider.getString(R.string.message_reply_to_sender_sent_file) + ) + } + repliedToEvent.isVoiceMessage() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + stringProvider.getString(R.string.message_reply_to_sender_sent_voice_message) + ) + } + repliedToEvent.isAudioMessage() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + stringProvider.getString(R.string.message_reply_to_sender_sent_audio_file) + ) + } + repliedToEvent.isImageMessage() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + stringProvider.getString(R.string.message_reply_to_sender_sent_image) + ) + } + repliedToEvent.isVideoMessage() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + stringProvider.getString(R.string.message_reply_to_sender_sent_video) + ) + } + repliedToEvent.isSticker() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + stringProvider.getString(R.string.message_reply_to_sender_sent_sticker) + ) + } + repliedToEvent.isPoll() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + repliedToEvent.getPollQuestion() ?: stringProvider.getString(R.string.message_reply_to_sender_created_poll) + ) + } + repliedToEvent.isLiveLocation() -> { + matrixFormattedBody.replaceRange( + afterBreakingLineIndex, + endOfBlockQuoteIndex, + stringProvider.getString(R.string.live_location_description) + ) + } + else -> matrixFormattedBody + } + } else { + matrixFormattedBody + } + + return withTranslatedContent.replace( + IN_REPLY_TO, + stringProvider.getString(R.string.message_reply_to_prefix) + ) + } + + private fun getEvent(eventId: String, roomId: String) = + activeSessionHolder.getSafeActiveSession() + ?.getRoom(roomId) + ?.getTimelineEvent(eventId) + ?.root +} From 57e90aee83ec735f6b189895f6262f5683d28831 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 10 Nov 2022 15:40:50 +0100 Subject: [PATCH 206/679] Removing description parameter in startLiveLocation method of SDK to avoid translated strings in beacon events --- .../api/session/room/location/LocationSharingService.kt | 3 +-- .../room/location/DefaultLocationSharingService.kt | 3 +-- .../session/room/location/StartLiveLocationShareTask.kt | 3 +-- .../room/location/DefaultLocationSharingServiceTest.kt | 9 +++------ .../location/DefaultStartLiveLocationShareTaskTest.kt | 6 +----- .../live/tracking/LocationSharingAndroidService.kt | 6 +----- 6 files changed, 8 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index cd8acbcccc..93208be27b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -47,10 +47,9 @@ interface LocationSharingService { /** * Starts sharing live location in the room. * @param timeoutMillis timeout of the live in milliseconds - * @param description description of the live for text fallback * @return the result of the update of the live */ - suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult + suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult /** * Stops sharing live location in the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 60312071d7..c36efa064f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -73,7 +73,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return sendLiveLocationTask.execute(params) } - override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult { + override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult { // Ensure to stop any active live before starting a new one if (checkIfExistingActiveLive()) { val result = stopLiveLocationShare() @@ -84,7 +84,6 @@ internal class DefaultLocationSharingService @AssistedInject constructor( val params = StartLiveLocationShareTask.Params( roomId = roomId, timeoutMillis = timeoutMillis, - description = description ) return startLiveLocationShareTask.execute(params) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index 79019e4765..781def1abe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -30,7 +30,6 @@ internal interface StartLiveLocationShareTask : Task From 58d182aecbb8a18d57208954e06b7715f9523864 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 10 Nov 2022 16:46:16 +0100 Subject: [PATCH 207/679] Adding unit tests on ProcessBodyOfReplyToEventUseCase --- .../ProcessBodyOfReplyToEventUseCase.kt | 2 - .../ProcessBodyOfReplyToEventUseCaseTest.kt | 268 ++++++++++++++++++ .../app/test/fakes/FakeTimelineService.kt | 2 +- 3 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt index 44fd5a397c..b22114a502 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt @@ -37,7 +37,6 @@ private const val IN_REPLY_TO = "In reply to" private const val BREAKING_LINE = "
" private const val ENDING_BLOCK_QUOTE = "" -// TODO add unit tests class ProcessBodyOfReplyToEventUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, private val stringProvider: StringProvider, @@ -48,7 +47,6 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor( val breakingLineIndex = matrixFormattedBody.lastIndexOf(BREAKING_LINE) val endOfBlockQuoteIndex = matrixFormattedBody.lastIndexOf(ENDING_BLOCK_QUOTE) - // TODO check in other platform how is handled the case of no repliedToEvent fetched val withTranslatedContent = if (repliedToEvent != null && breakingLineIndex != -1 && endOfBlockQuoteIndex != -1) { val afterBreakingLineIndex = breakingLineIndex + BREAKING_LINE.length when { diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt new file mode 100644 index 0000000000..f612861511 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.render + +import android.annotation.StringRes +import im.vector.app.R +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeStringProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.getPollQuestion +import org.matrix.android.sdk.api.session.events.model.isAudioMessage +import org.matrix.android.sdk.api.session.events.model.isFileMessage +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isLiveLocation +import org.matrix.android.sdk.api.session.events.model.isPoll +import org.matrix.android.sdk.api.session.events.model.isSticker +import org.matrix.android.sdk.api.session.events.model.isVideoMessage +import org.matrix.android.sdk.api.session.events.model.isVoiceMessage +import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +private const val A_ROOM_ID = "room-id" +private const val AN_EVENT_ID = "event-id" +private const val A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY = + "" + + "
" + + "In reply to " + + "@user:matrix.org" + + "
" + + "Message content" + + "
" + + "
" + + "Reply text" +private const val A_NEW_PREFIX = "new-prefix" +private const val A_NEW_CONTENT = "new-content" +private const val PREFIX_PROCESSED_ONLY_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY = + "" + + "
" + + "$A_NEW_PREFIX " + + "@user:matrix.org" + + "
" + + "Message content" + + "
" + + "
" + + "Reply text" +private const val FULLY_PROCESSED_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY = + "" + + "
" + + "$A_NEW_PREFIX " + + "@user:matrix.org" + + "
" + + A_NEW_CONTENT + + "
" + + "
" + + "Reply text" + +class ProcessBodyOfReplyToEventUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeStringProvider = FakeStringProvider() + private val fakeReplyToContent = ReplyToContent(eventId = AN_EVENT_ID) + private val fakeRepliedEvent = givenARepliedEvent() + + private val processBodyOfReplyToEventUseCase = ProcessBodyOfReplyToEventUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + stringProvider = fakeStringProvider.instance, + ) + + @Before + fun setup() { + givenNewPrefix() + mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a replied event of type file message when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isFileMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_sent_file) + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type voice message when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isVoiceMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_sent_voice_message) + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type audio message when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isAudioMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_sent_audio_file) + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type image message when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isImageMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_sent_image) + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type video message when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isVideoMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_sent_video) + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type sticker message when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isStickerMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_sent_sticker) + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type poll message with null question when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isPollMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_created_poll) + every { fakeRepliedEvent.getPollQuestion() } returns null + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type poll message with existing question when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isPollMessage = true) + givenNewContentForId(R.string.message_reply_to_sender_created_poll) + every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent(isLiveLocationMessage = true) + givenNewContentForId(R.string.live_location_description) + + executeAndAssertResult() + } + + @Test + fun `given a replied event of type not handled when process the formatted body only prefix is replaced by correct string`() { + // Given + givenTypeOfRepliedEvent() + + // When + val result = processBodyOfReplyToEventUseCase.execute( + roomId = A_ROOM_ID, + matrixFormattedBody = A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY, + replyToContent = fakeReplyToContent, + ) + + // Then + result shouldBeEqualTo PREFIX_PROCESSED_ONLY_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY + } + + @Test + fun `given no replied event found when process the formatted body then only prefix is replaced by correct string`() { + // Given + givenARepliedEvent(timelineEvent = null) + + // When + val result = processBodyOfReplyToEventUseCase.execute( + roomId = A_ROOM_ID, + matrixFormattedBody = A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY, + replyToContent = fakeReplyToContent, + ) + + // Then + result shouldBeEqualTo PREFIX_PROCESSED_ONLY_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY + } + + private fun executeAndAssertResult() { + // When + val result = processBodyOfReplyToEventUseCase.execute( + roomId = A_ROOM_ID, + matrixFormattedBody = A_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY, + replyToContent = fakeReplyToContent, + ) + + // Then + result shouldBeEqualTo FULLY_PROCESSED_REPLY_TO_EVENT_MATRIX_FORMATTED_BODY + } + + private fun givenARepliedEvent(timelineEvent: TimelineEvent? = mockk()): Event { + val event = mockk() + timelineEvent?.let { every { it.root } returns event } + fakeActiveSessionHolder + .fakeSession + .roomService() + .getRoom(A_ROOM_ID) + .timelineService() + .givenTimelineEvent(timelineEvent) + return event + } + + private fun givenTypeOfRepliedEvent( + isFileMessage: Boolean = false, + isVoiceMessage: Boolean = false, + isAudioMessage: Boolean = false, + isImageMessage: Boolean = false, + isVideoMessage: Boolean = false, + isStickerMessage: Boolean = false, + isPollMessage: Boolean = false, + isLiveLocationMessage: Boolean = false, + ) { + every { fakeRepliedEvent.isFileMessage() } returns isFileMessage + every { fakeRepliedEvent.isVoiceMessage() } returns isVoiceMessage + every { fakeRepliedEvent.isAudioMessage() } returns isAudioMessage + every { fakeRepliedEvent.isImageMessage() } returns isImageMessage + every { fakeRepliedEvent.isVideoMessage() } returns isVideoMessage + every { fakeRepliedEvent.isSticker() } returns isStickerMessage + every { fakeRepliedEvent.isPoll() } returns isPollMessage + every { fakeRepliedEvent.isLiveLocation() } returns isLiveLocationMessage + } + + private fun givenNewPrefix() { + fakeStringProvider.given(R.string.message_reply_to_prefix, A_NEW_PREFIX) + } + + private fun givenNewContentForId(@StringRes resId: Int) { + fakeStringProvider.given(resId, A_NEW_CONTENT) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt index 56f38724b1..a5fac5f1a1 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTimelineService.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService class FakeTimelineService : TimelineService by mockk() { - fun givenTimelineEvent(event: TimelineEvent) { + fun givenTimelineEvent(event: TimelineEvent?) { every { getTimelineEvent(any()) } returns event } } From 008432af3687baf53f0dc32824dee27fd57b8d69 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 10 Nov 2022 18:28:03 +0100 Subject: [PATCH 208/679] Move TypingView into the timeline as another item (#7565) * Typing view as item in list * Don't show TypingItem if we're showing a forward loader --- changelog.d/7496.feature | 1 + .../app/core/ui/views/TypingMessageView.kt | 5 -- .../home/room/detail/TimelineFragment.kt | 12 --- .../timeline/TimelineEventController.kt | 15 +++- .../room/detail/timeline/item/TypingItem.kt | 76 +++++++++++++++++++ .../src/main/res/layout/fragment_timeline.xml | 14 +--- .../src/main/res/layout/item_typing_users.xml | 8 ++ 7 files changed, 98 insertions(+), 33 deletions(-) create mode 100644 changelog.d/7496.feature create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt create mode 100644 vector/src/main/res/layout/item_typing_users.xml diff --git a/changelog.d/7496.feature b/changelog.d/7496.feature new file mode 100644 index 0000000000..721164ee06 --- /dev/null +++ b/changelog.d/7496.feature @@ -0,0 +1 @@ +Move TypingView inside the timeline items. diff --git a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt index 263f043fad..b6dc404d01 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt @@ -48,9 +48,4 @@ class TypingMessageView @JvmOverloads constructor( views.typingUserText.text = typingHelper.getNotificationTypingMessage(typingUsers) views.typingUserAvatars.render(typingUsers, avatarRenderer) } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - removeAllViews() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 60dd1320d3..e1392b7580 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1154,7 +1154,6 @@ class TimelineFragment : } val summary = mainState.asyncRoomSummary() renderToolbar(summary) - renderTypingMessageNotification(summary, mainState) views.removeJitsiWidgetView.render(mainState) if (mainState.hasFailedSending) { lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true @@ -1230,17 +1229,6 @@ class TimelineFragment : voiceMessageRecorderContainer.isVisible = false } - private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) { - if (!isThreadTimeLine() && roomSummary != null) { - views.typingMessageView.isInvisible = state.typingUsers.isNullOrEmpty() - state.typingUsers - ?.take(MAX_TYPING_MESSAGE_USERS_COUNT) - ?.let { senders -> views.typingMessageView.render(senders, avatarRenderer) } - } else { - views.typingMessageView.isInvisible = true - } - } - private fun renderToolbar(roomSummary: RoomSummary?) { when { isLocalRoom() -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 18c626bda8..57ad4331ce 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull import im.vector.app.core.time.Clock +import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState @@ -57,6 +58,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem +import im.vector.app.features.home.room.detail.timeline.item.TypingItem_ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.ImageContentRenderer @@ -94,6 +96,7 @@ class TimelineEventController @Inject constructor( private val readReceiptsItemFactory: ReadReceiptsItemFactory, private val reactionListFactory: ReactionsSummaryFactory, private val clock: Clock, + private val avatarRenderer: AvatarRenderer, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { /** @@ -104,7 +107,7 @@ class TimelineEventController @Inject constructor( val highlightedEventId: String? = null, val jitsiState: JitsiState = JitsiState(), val roomSummary: RoomSummary? = null, - val rootThreadEventId: String? = null + val rootThreadEventId: String? = null, ) { constructor(state: RoomDetailViewState) : this( @@ -112,7 +115,7 @@ class TimelineEventController @Inject constructor( highlightedEventId = state.highlightedEventId, jitsiState = state.jitsiState, roomSummary = state.asyncRoomSummary(), - rootThreadEventId = state.rootThreadEventId + rootThreadEventId = state.rootThreadEventId, ) fun isFromThreadTimeline(): Boolean = rootThreadEventId != null @@ -286,7 +289,7 @@ class TimelineEventController @Inject constructor( private val interceptorHelper = TimelineControllerInterceptorHelper( ::positionOfReadMarker, - adapterPositionMapping + adapterPositionMapping, ) init { @@ -334,6 +337,12 @@ class TimelineEventController @Inject constructor( .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) .addWhenLoading(Timeline.Direction.FORWARDS) + if (!showingForwardLoader) { + val typingUsers = partialState.roomSummary?.typingUsers.orEmpty() + val typingItem = TypingItem_().id("typing_view").avatarRenderer(avatarRenderer).users(typingUsers) + add(typingItem) + } + val timelineModels = getModels() add(timelineModels) if (hasReachedInvite && hasUTD) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt new file mode 100644 index 0000000000..2ca0ebea48 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.ui.views.TypingMessageView +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +@EpoxyModelClass +abstract class TypingItem : EpoxyModelWithHolder() { + + companion object { + private const val MAX_TYPING_MESSAGE_USERS_COUNT = 4 + } + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var users: List = emptyList() + + override fun getDefaultLayout(): Int = R.layout.item_typing_users + + override fun bind(holder: TypingHolder) { + super.bind(holder) + + val typingUsers = users.take(MAX_TYPING_MESSAGE_USERS_COUNT) + holder.typingView.apply { + animate().cancel() + val duration = 100L + if (typingUsers.isEmpty()) { + animate().translationY(height.toFloat()) + .alpha(0f) + .setDuration(duration) + .withEndAction { + isInvisible = true + }.start() + } else { + isVisible = true + + translationY = height.toFloat() + alpha = 0f + render(typingUsers, avatarRenderer) + animate().translationY(0f) + .alpha(1f) + .setDuration(duration) + .start() + } + } + } + + class TypingHolder : VectorEpoxyHolder() { + val typingView by bind(R.id.typingMessageView) + } +} diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index 100cf694e0..2d07464e89 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -85,7 +85,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:overScrollMode="always" - app:layout_constraintBottom_toTopOf="@id/typingMessageView" + app:layout_constraintBottom_toTopOf="@id/bottomBarrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" @@ -107,18 +107,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" /> - - + From 8278ae61e57562aa689c006b8c6dc33066c3056a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 23:11:00 +0000 Subject: [PATCH 209/679] Bump flipper from 0.173.0 to 0.174.0 Bumps `flipper` from 0.173.0 to 0.174.0. Updates `flipper` from 0.173.0 to 0.174.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.173.0...v0.174.0) Updates `flipper-network-plugin` from 0.173.0 to 0.174.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.173.0...v0.174.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index af3ff72446..dc66de43ea 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.173.0" +def flipper = "0.174.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" From 9fd50b4e351e7bdeef59c8288f677a091579849b Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 11 Nov 2022 08:04:06 +0000 Subject: [PATCH 210/679] Translated using Weblate (Czech) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- library/ui-strings/src/main/res/values-cs/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index 47caa52149..b4f9698197 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2899,4 +2899,13 @@ Nelze zahájit nové hlasové vysílání Přetočení o 30 sekund zpět Přetočení o 30 sekund dopředu + Ověřené relace jsou všude tam, kde tento účet používáte po zadání přístupové fráze nebo po potvrzení své totožnosti jinou ověřenou relací. +\n +\nTo znamená, že máte všechny klíče potřebné k odemknutí zašifrovaných zpráv a potvrzení ostatním uživatelům, že této relaci důvěřujete. + + Odhlásit se z %1$d relace + Odhlásit se ze %1$d relací + Odhlásit se z %1$d relací + + Odhlásit se \ No newline at end of file From 47a909eaf4a34d4038e71113f4f931dd6824bb12 Mon Sep 17 00:00:00 2001 From: Vri Date: Thu, 10 Nov 2022 11:02:51 +0000 Subject: [PATCH 211/679] Translated using Weblate (German) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- library/ui-strings/src/main/res/values-de/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index cd215e175d..186533c8f9 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2843,4 +2843,12 @@ Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest. 30 Sekunden vorspulen 30 Sekunden zurückspulen + Auf verifizierte Sitzungen kannst du überall mit deinem Konto zugreifen, wenn du deine Passphrase eingegeben oder Element mit einer anderen Sitzung verifiziert hast. +\n +\nDies bedeutet, dass du alle Schlüssel zum Entsperren deiner verschlüsselten Nachrichten hast und anderen bestätigst, dieser Sitzung zu vertrauen. + + Von %1$d Sitzung abmelden + Von %1$d Sitzungen abmelden + + Abmelden \ No newline at end of file From 2d80a25fd6b36616d79ddc01d1a19b7410854a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 10 Nov 2022 21:01:22 +0000 Subject: [PATCH 212/679] Translated using Weblate (Estonian) Currently translated at 99.6% (2534 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- library/ui-strings/src/main/res/values-et/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 22572a0f36..4747f03fee 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2835,4 +2835,12 @@ Uue ringhäälingukõne alustamine pole võimalik Keri tagasi 30 sekundi kaupa Keri edasi 30 sekundi kaupa + Verifitseeritud sessioonideks loetakse Element\'is või mõnes muus Matrix\'i rakenduses selliseid sessioone, kus sa kas oled sisestanud oma salafraasi või tuvastanud end mõne teise oma verifitseeritud sessiooni abil. +\n +\nSee tähendab, et selles sessioonis on ka kõik vajalikud võtmed krüptitud sõnumite lugemiseks ja teistele kasutajatele kinnitamiseks, et sa usaldad seda sessiooni. + + Logi välja %1$d\'st sessioonist + Logi välja %1$d\'st sessioonist + + Logi välja \ No newline at end of file From 8b5f86ec51b2e76eb9c713059b83e60e2f7d0972 Mon Sep 17 00:00:00 2001 From: Glandos Date: Sat, 12 Nov 2022 10:37:18 +0000 Subject: [PATCH 213/679] Translated using Weblate (French) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- library/ui-strings/src/main/res/values-fr/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index a02b062596..a8b5eb28ae 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2844,4 +2844,12 @@ Impossible de commencer une nouvelle diffusion audio Avance rapide de 30 secondes Retour rapide de 30 secondes + Les sessions vérifiées sont toutes celles qui utilisent ce compte après avoir saisie la phrase de sécurité ou confirmé votre identité à l’aide d’une autre session vérifiée. +\n +\nCela veut dire qu’elles disposent de toutes les clés nécessaires pour lire les messages chiffrés, et confirment aux autres utilisateur que vous faites confiance à cette session. + + Déconnecter %1$d session + Déconnecter %1$d sessions + + Déconnecter \ No newline at end of file From e5783afb115a5543424dd31184075abd7d14fcb9 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 12 Nov 2022 09:20:12 +0000 Subject: [PATCH 214/679] Translated using Weblate (Hungarian) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/hu/ --- .../src/main/res/values-hu/strings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml index 21ea6aab14..8f449dadaf 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -2836,4 +2836,20 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze %1$d kiválasztva %1$d kiválasztva + Teljes képernyő váltás + Mindenhol ellenőrzött munkamenetek vannak ahol ezt a fiókot használva megadtad a jelmondatodat vagy egy másik már hitelesített munkamenetből megerősítetted az identitásodat. +\n +\nEz azt jelenti, hogy a titkosított üzenetek visszafejtéséhez rendelkezel a kulcsokkal és megerősíted a többiek felé, hogy megbízol a munkamenetben. + + Kijelentkezés %1$d munkamenetből + Kijelentkezés %1$d munkamenetből + + Kijelentkezés + Szöveg formázás + Egy hang közvetítés már folyamatban van. Először fejezze be a jelenlegi közvetítést egy új indításához. + Valaki már elindított egy hang közvetítést. Várd meg a közvetítés végét az új indításához. + Nincs jogosultságod hang közvetítést indítani ebben a szobában. Vedd fel a kapcsolatot a szoba adminisztrátorával a szükséges jogosultság megszerzéséhez. + Az új hang közvetítés nem indítható el + 30 másodperccel előre + 30 másodperccel vissza \ No newline at end of file From ac06ff52569b13185de0f8f50bf4a395e38b6b44 Mon Sep 17 00:00:00 2001 From: Linerly Date: Sat, 12 Nov 2022 06:50:29 +0000 Subject: [PATCH 215/679] Translated using Weblate (Indonesian) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/id/ --- library/ui-strings/src/main/res/values-in/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index cde367faf9..8f7670d08f 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2791,4 +2791,11 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Tidak dapat memulai siaran suara baru Maju cepat 30 detik Mundur cepat 30 detik + Sesi terverifikasi ada di mana pun Anda menggunakan Element setelah memasukkan frasa sandi atau mengonfirmasi identitas Anda dengan sesi terverifikasi lainnya. +\n +\nIni berarti Anda memiliki semua kunci yang diperlukan untuk membuka kunci pesan terenkripsi dan mengonfirmasi kepada pengguna lain bahwa Anda memercayai sesi ini. + + Keluarkan %1$d sesi + + Keluarkan \ No newline at end of file From 6c52e47e473eb0a8287ddd78596a7110f45c2658 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 11 Nov 2022 14:08:42 +0000 Subject: [PATCH 216/679] Translated using Weblate (Italian) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/it/ --- .../src/main/res/values-it/strings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index 5cc509e8b9..4257d52c3e 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -2827,4 +2827,20 @@ %1$d selezionato %1$d selezionati + Attiva/disattiva schermo intero + Le sessioni verificate sono ovunque usi questo account dopo l\'inserimento della password o la conferma della tua identità con un\'altra sessione verificata. +\n +\nCiò significa che hai tutte le chiavi necessarie per sbloccare i tuoi messaggi cifrati e per confermare agli altri utenti che ti fidi di questa sessione. + + Disconnetti da %1$d sessione + Disconnetti da %1$d sessioni + + Disconnetti + Formattazione testo + Stai già registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova. + Qualcun altro sta già registrando una trasmissione vocale. Aspetta che finisca prima di iniziarne una nuova. + Non hai l\'autorizzazione necessaria per iniziare una trasmissione vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni. + Impossibile iniziare una nuova trasmissione vocale + Manda avanti di 30 secondi + Manda indietro di 30 secondi \ No newline at end of file From 24868a6c6404d7b408d8184611e89ac09191cc7e Mon Sep 17 00:00:00 2001 From: Platon Terekhov Date: Fri, 11 Nov 2022 19:26:31 +0000 Subject: [PATCH 217/679] Translated using Weblate (Russian) Currently translated at 95.3% (2423 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- .../src/main/res/values-ru/strings.xml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index fb819efb69..cdac891840 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -2805,4 +2805,30 @@ Местоположение Камера Контакт + ${app_name} нуждается в разрешении для отображения оповещений. +\nПожалуйста, дайте разрешение. + + %1$s и %2$d другой + %1$s и %2$d другие + %1$s и %2$d других + %1$s и %2$d других + + ${app_name} нуждается в резрешении для отображения оповещений. Оповещения могут показывать ваши сообщения, приглашения и тому подобное. +\n +\nПожалуйста разрешите доступ при следующем всплывающем сообщении, чтобы иметь возможность видеть оповещения. + Здесь будут появляться новые запросы и приглашения. + Приглашения + Попробуйте расширенный текстовый редактор (режим набора обычного текста скоро появится) + Создавать личные сообщения только при отправке первого сообщения + Включить отложенные личные сообщения + Отменить выбор всего + Выбрать всё + Свернуть дочерние элементы %s + Развернуть дочерние элементы %s + + Выбрано %1$d + Выбрано %1$d + Выбрано %1$d + Выбрано %1$d + \ No newline at end of file From 6fa15e6ee90dd6be7a1f7aa341dd1171947ce3ab Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Thu, 10 Nov 2022 16:33:24 +0000 Subject: [PATCH 218/679] Translated using Weblate (Slovak) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index 9eac092a62..9c15c5ec0d 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2765,7 +2765,7 @@ Zapnúť nové usporiadanie Ostatní používatelia v priamych správach a miestnostiach, do ktorých sa pripojíte, si môžu pozrieť úplný zoznam vašich relácií. \n -\nTo im poskytuje istotu, že sa s vami naozaj rozprávajú, ale zároveň to znamená, že vidia názov relácie, ktorý sem zadáte. +\nTo im poskytuje istotu, že sa komunikujú naozaj s vami, ale zároveň to znamená, že vidia názov relácie, ktorý sem zadáte. Premenovanie relácií Overené relácie, do ktorých ste sa prihlásili pomocou svojich prihlasovacích údajov a ktoré boli následne overené buď pomocou vašej bezpečnostnej prístupovej frázy, alebo krížovým overením. \n @@ -2899,4 +2899,13 @@ Nie je možné spustiť nové hlasové vysielanie Rýchle posunutie dozadu o 30 sekúnd Rýchle posunutie dopredu o 30 sekúnd + Overené relácie sú všade tam, kde používate toto konto po zadaní svojho prístupového hesla alebo po potvrdení svojej totožnosti inou overenou reláciou. +\n +\nTo znamená, že máte všetky kľúče potrebné na odomknutie zašifrovaných správ a potvrdenie pre ostatných používateľov, že tejto relácii dôverujete. + + Odhlásiť sa z %1$d relácie + Odhlásiť sa z %1$d relácií + Odhlásiť sa z %1$d relácií + + Odhlásiť sa \ No newline at end of file From baaace855c8c9db97e8d05611e432d714e63ae25 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 10 Nov 2022 14:55:18 +0000 Subject: [PATCH 219/679] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- .../src/main/res/values-uk/strings.xml | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 8cbfeca6ba..8affc63685 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2839,12 +2839,12 @@ Перейменувати сеанс Вийти з цього сеансу Не звірений - Ваш поточний сеанс - Розпочати трансляцію голосового повідомлення + Розпочати голосову трансляцію Справжність цього зашифрованого повідомлення не може бути гарантована на цьому пристрої. Заборонити клавіатурі оновлювати будь-які персоналізовані дані, як-от історію набору тексту та словник, на основі того, що ви набрали в розмовах. Зверніть увагу, що деякі клавіатури можуть не дотримуватися цього налаштування. Клавіатура інкогніто Надсилає (╯°□°)╯︵ ┻━┻ на початку текстового повідомлення - Голосові повідомлення + Голосові трансляції Відкрийте інструменти розробника 🔒 Ви увімкнули шифрування лише для перевірених сеансів для всіх кімнат у налаштуваннях безпеки. ⚠ У цій кімнаті є неперевірені пристрої, вони не зможуть розшифрувати повідомлення, які ви надсилаєте. @@ -2920,21 +2920,21 @@ Вхід з іншого пристрою вже виконано. Під час налаштування захищеного обміну повідомленнями виникла проблема з безпекою. Можливо, порушено одне з таких налаштувань: Ваш домашній сервер; Ваше інтернет-з\'єднання; Ваш пристрій; Запит не виконаний. - Можливість записувати та надсилати голосові повідомлення до стрічки кімнати. - Увімкнути голосові повідомлення (в активній розробці) + Можливість записувати та надсилати голосові трансляції до стрічки кімнати. + Увімкнути голосові трансляції (в активній розробці) Буферизація - Призупинити голосове повідомлення - Відтворити або поновити відтворення голосового повідомлення - Припинити запис голосового повідомлення - Призупинити запис голосового повідомлення - Відновити запис голосового повідомлення + Призупинити голосову трансляцію + Відтворити або поновити відтворення голосової трансляції + Припинити запис голосової трансляції + Призупинити запис голосової трансляції + Відновити запис голосової трансляції Наживо Вибрати сеанси Контакт Камера Місце перебування Опитування - Голосові повідомлення + Голосові трансляції Вкладення Наліпки Фотобібліотека @@ -2948,10 +2948,20 @@ Вибрати все Перемкнути повноекранний режим Форматування тексту - Ви вже записуєте голосове повідомлення. Завершіть поточну трансляцію, щоб розпочати нову. - Хтось інший вже записує голосове повідомлення. Зачекайте, поки закінчиться трансляція, щоб розпочати нову. - Ви не маєте необхідних дозволів для початку передавання голосового повідомлення в цю кімнату. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи. - Не вдалося розпочати передавання нового голосового повідомлення + Ви вже записуєте голосову трансляцію. Завершіть поточну трансляцію, щоб розпочати нову. + Хтось інший вже записує голосову трансляцію. Зачекайте, поки вона завершиться, щоб розпочати нову. + Ви не маєте необхідних дозволів для початку голосової трансляції в цю кімнату. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи. + Не вдалося розпочати нову голосову трансляцію Перемотати вперед на 30 секунд Перемотати назад на 30 секунд + Звірені сеанси — це будь-який пристрій, на якому ви використовуєте цей обліковий запис після введення парольної фрази або підтвердження вашої особи за допомогою іншого звіреного сеансу. +\n +\nЦе означає, що ви маєте всі ключі, необхідні для розблокування ваших зашифрованих повідомлень і підтвердження іншим користувачам, що ви довіряєте цьому сеансу. + + Вийти з %1$d сеансу + Вийти з %1$d сеансів + Вийти з %1$d сеансів + Вийти з %1$d сеансів + + Вийти \ No newline at end of file From a616ab9472b9575bd50c97a9e74b3e5c90b5366a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 11 Nov 2022 02:09:46 +0000 Subject: [PATCH 220/679] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/ --- library/ui-strings/src/main/res/values-zh-rTW/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index 91e08c803a..6976854649 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2789,4 +2789,11 @@ 無法開始新的語音廣播 快轉30秒 快退30秒 + 已驗證的工作階段是您輸入通關密語或透過另一個已驗證工作階段確認您的身份後使用此帳號的任何地方。 +\n +\n這代表了您擁有解鎖加密訊息並向其他使用者確認您信任此工作階段所需的所有金鑰。 + + 登出 %1$d 個工作階段 + + 登出 \ No newline at end of file From 4578336245ca0816aa3855470f7a412b77255810 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 10 Nov 2022 14:50:21 +0000 Subject: [PATCH 221/679] Translated using Weblate (Ukrainian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/ --- fastlane/metadata/android/uk/changelogs/40105040.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/uk/changelogs/40105040.txt b/fastlane/metadata/android/uk/changelogs/40105040.txt index bbc005f84c..b3327f68ab 100644 --- a/fastlane/metadata/android/uk/changelogs/40105040.txt +++ b/fastlane/metadata/android/uk/changelogs/40105040.txt @@ -1,2 +1,2 @@ -Основні зміни в цій версії: Нові можливості в налаштуваннях лабораторії: Текстовий редактор, нове керування пристроями, голосові повідомлення. Досі в активній розробці! +Основні зміни в цій версії: Нові можливості в налаштуваннях лабораторії: Текстовий редактор, нове керування пристроями, голосові трансляції. Досі в активній розробці! Список усіх змін: https://github.com/vector-im/element-android/releases From 227fb27a2ed9ede2fd9fa85c72668cc05f3bcd75 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 11 Nov 2022 14:09:50 +0000 Subject: [PATCH 222/679] Translated using Weblate (Italian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/it/ --- fastlane/metadata/android/it-IT/changelogs/40105060.txt | 2 ++ fastlane/metadata/android/it-IT/changelogs/40105070.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/40105060.txt create mode 100644 fastlane/metadata/android/it-IT/changelogs/40105070.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/40105060.txt b/fastlane/metadata/android/it-IT/changelogs/40105060.txt new file mode 100644 index 0000000000..34d299b774 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: nuova interfaccia utente per selezionare un allegato! +Cronologia completa: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/it-IT/changelogs/40105070.txt b/fastlane/metadata/android/it-IT/changelogs/40105070.txt new file mode 100644 index 0000000000..ec4d944d72 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: nuova interfaccia utente per selezionare un allegato. +Cronologia completa: https://github.com/vector-im/element-android/releases From fcfef53043856b2c4f2ddbcc6265032aad530605 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 14 Nov 2022 10:12:25 +0100 Subject: [PATCH 223/679] Search for the first occurrence (and not last) of breaking line just in case --- .../detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt index b22114a502..2197d89a2c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt @@ -44,7 +44,7 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor( fun execute(roomId: String, matrixFormattedBody: String, replyToContent: ReplyToContent): String { val repliedToEvent = replyToContent.eventId?.let { getEvent(it, roomId) } - val breakingLineIndex = matrixFormattedBody.lastIndexOf(BREAKING_LINE) + val breakingLineIndex = matrixFormattedBody.indexOf(BREAKING_LINE) val endOfBlockQuoteIndex = matrixFormattedBody.lastIndexOf(ENDING_BLOCK_QUOTE) val withTranslatedContent = if (repliedToEvent != null && breakingLineIndex != -1 && endOfBlockQuoteIndex != -1) { From 4a65e1153abcd25a8927340cc3f20c396c9d2ef1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 14 Nov 2022 10:18:42 +0100 Subject: [PATCH 224/679] Fix retrieve of the question for poll events --- .../org/matrix/android/sdk/api/session/events/model/Event.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 1c09b49298..720e6f3deb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -447,7 +447,7 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && content?.toModel()?.membership == Membership.INVITE fun Event.getPollContent(): MessagePollContent? { - return getDecryptedContent().toModel() + return getClearContent().toModel() } fun Event.supportsNotification() = From 6ee1e869513c7b2237d9e7e93119adc86142a508 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 00:44:05 +0100 Subject: [PATCH 225/679] Improve live indicator --- .../item/AbsMessageVoiceBroadcastItem.kt | 31 ++++++++++--------- .../MessageVoiceBroadcastListeningItem.kt | 12 +++++++ .../MessageVoiceBroadcastRecordingItem.kt | 9 ++++++ .../listening/VoiceBroadcastPlayer.kt | 5 +++ .../listening/VoiceBroadcastPlayerImpl.kt | 25 ++++++++++++--- .../listening/VoiceBroadcastPlaylist.kt | 7 +++-- 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 0329adf12b..c6b90cdabe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -68,25 +68,26 @@ abstract class AbsMessageVoiceBroadcastItem { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.PAUSED -> { - liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) - liveIndicator.isVisible = true - } - VoiceBroadcastState.STOPPED, null -> { - liveIndicator.isVisible = false - } - } + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError)) + liveIndicator.isVisible = true + } + } + + protected fun renderPausedLiveIndicator(holder: H) { + with(holder) { + liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary)) + liveIndicator.isVisible = true } } + protected fun renderNoLiveIndicator(holder: H) { + holder.liveIndicator.isVisible = false + } + abstract fun renderMetadata(holder: H) abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 4b91bbfb0e..b114f95f97 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -29,6 +29,7 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView @EpoxyModelClass @@ -82,6 +83,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } } + override fun renderLiveIndicator(holder: Holder) { + when { + voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED -> renderNoLiveIndicator(holder) + voiceBroadcastState == VoiceBroadcastState.PAUSED || !player.isLiveListening -> renderPausedLiveIndicator(holder) + else -> renderPlayingLiveIndicator(holder) + } + } + private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) { with(holder) { bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING @@ -99,6 +108,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } VoiceBroadcastPlayer.State.BUFFERING -> Unit } + + renderLiveIndicator(holder) } } @@ -121,6 +132,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState -> renderBackwardForwardButtons(holder, playbackState) + renderLiveIndicator(holder) if (!isUserSeeking) { holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 17aa1543c0..ed77452382 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -48,6 +48,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } } + override fun renderLiveIndicator(holder: Holder) { + when (voiceBroadcastState) { + VoiceBroadcastState.STARTED, + VoiceBroadcastState.RESUMED -> renderPlayingLiveIndicator(holder) + VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder) + VoiceBroadcastState.STOPPED, null -> renderNoLiveIndicator(holder) + } + } + override fun renderMetadata(holder: Holder) { with(holder) { listenersCountMetadata.isVisible = false diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 8c11db4f43..02e843965f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -30,6 +30,11 @@ interface VoiceBroadcastPlayer { */ val playingState: State + /** + * Tells whether the player is listening a live voice broadcast in "live" position. + */ + val isLiveListening: Boolean + /** * Start playback of the given voice broadcast. */ diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 6a6dc6a9e8..573a178c78 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -30,7 +30,6 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.Job @@ -70,6 +69,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null override var currentVoiceBroadcast: VoiceBroadcast? = null + override var isLiveListening: Boolean = false override var playingState = State.IDLE @MainThread @@ -142,7 +142,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) - .onEach { currentVoiceBroadcastEvent = it.getOrNull() } + .onEach { + currentVoiceBroadcastEvent = it.getOrNull() + updateLiveListeningMode() + } .launchIn(sessionScope) } @@ -190,7 +193,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = playlistItem.audioEvent.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return } + val sequence = playlistItem.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return } val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 sessionScope.launch { try { @@ -241,6 +244,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) } playingState == State.PLAYING || playingState == State.BUFFERING -> { + updateLiveListeningMode(positionMillis) startPlayback(positionMillis) } playingState == State.IDLE || playingState == State.PAUSED -> { @@ -302,18 +306,31 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun onPlayingStateChanged(playingState: State) { - // Notify state change to all the listeners attached to the current voice broadcast id + // Update live playback flag + updateLiveListeningMode() + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> + // Start or stop playback ticker when (playingState) { State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId) State.PAUSED, State.BUFFERING, State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) } + // Notify state change to all the listeners attached to the current voice broadcast id listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) } } } + private fun updateLiveListeningMode(playbackPosition: Int? = null) { + isLiveListening = when { + !currentVoiceBroadcastEvent?.isLive.orFalse() -> false + playingState == State.IDLE || playingState == State.PAUSED -> false + playbackPosition != null -> playlist.findByPosition(playbackPosition)?.sequence == playlist.lastOrNull()?.sequence + else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence + } + } + private fun getCurrentPlaybackPosition(): Int? { val playlistPosition = playlist.currentItem?.startTime val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt index ff388c2313..36b737f23f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -56,7 +56,7 @@ class VoiceBroadcastPlaylist( } fun findBySequence(sequenceNumber: Int): PlaylistItem? { - return items.find { it.audioEvent.sequence == sequenceNumber } + return items.find { it.sequence == sequenceNumber } } fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1) @@ -64,4 +64,7 @@ class VoiceBroadcastPlaylist( fun firstOrNull() = findBySequence(1) } -data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) +data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) { + val sequence: Int? + get() = audioEvent.sequence +} From 5eb260e674384cbcfa7e2771d6948a8b010ffb5e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 14:11:34 +0100 Subject: [PATCH 226/679] Unregister listeners on recording tile --- .../timeline/item/MessageVoiceBroadcastRecordingItem.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index ed77452382..9bd6fc45ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -113,6 +113,10 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem super.unbind(holder) recorderListener?.let { recorder?.removeListener(it) } recorderListener = null + with(holder) { + recordButton.onClick(null) + stopRecordButton.onClick(null) + } } override fun getViewStubId() = STUB_ID From 2d006f87256644087409bbc102156e7b1894055e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 15:59:28 +0100 Subject: [PATCH 227/679] Bind listener to live playback flag --- .../MessageVoiceBroadcastListeningItem.kt | 10 +++++- .../listening/VoiceBroadcastPlayer.kt | 9 +++-- .../listening/VoiceBroadcastPlayerImpl.kt | 33 ++++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index b114f95f97..7c7e69f320 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -44,7 +44,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) } + playerListener = object : VoiceBroadcastPlayer.Listener { + override fun onPlayingStateChanged(state: VoiceBroadcastPlayer.State) { + renderPlayingState(holder, state) + } + + override fun onLiveModeChanged(isLive: Boolean) { + renderLiveIndicator(holder) + } + } player.addListener(voiceBroadcast, playerListener) bindSeekBar(holder) bindButtons(holder) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 02e843965f..0de88e9992 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -78,10 +78,15 @@ interface VoiceBroadcastPlayer { /** * Listener related to [VoiceBroadcastPlayer]. */ - fun interface Listener { + interface Listener { /** * Notify about [VoiceBroadcastPlayer.playingState] changes. */ - fun onStateChanged(state: State) + fun onPlayingStateChanged(state: State) = Unit + + /** + * Notify about [VoiceBroadcastPlayer.isLiveListening] changes. + */ + fun onLiveModeChanged(isLive: Boolean) = Unit } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 573a178c78..56c80ddfb1 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -121,7 +121,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run { listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } - listener.onStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) + listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) + listener.onLiveModeChanged(if (voiceBroadcast == currentVoiceBroadcast) isLiveListening else false) } override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { @@ -318,17 +319,41 @@ class VoiceBroadcastPlayerImpl @Inject constructor( State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) } // Notify state change to all the listeners attached to the current voice broadcast id - listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) } + listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) } } } - private fun updateLiveListeningMode(playbackPosition: Int? = null) { + /** + * Update the live listening state according to: + * - the voice broadcast state, + * - the playing state, + * - the potential seek position. + */ + private fun updateLiveListeningMode(seekPosition: Int? = null) { isLiveListening = when { + // the current voice broadcast is not live (ended) !currentVoiceBroadcastEvent?.isLive.orFalse() -> false + // the player is stopped or paused playingState == State.IDLE || playingState == State.PAUSED -> false - playbackPosition != null -> playlist.findByPosition(playbackPosition)?.sequence == playlist.lastOrNull()?.sequence + // the user has sought + seekPosition != null -> { + val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0) + when { + // backward + seekDirection < 0 -> false + // forward: check if new sequence is the last one + else -> playlist.findByPosition(seekPosition)?.sequence == playlist.lastOrNull()?.sequence + } + } + // otherwise, stay in live or go in live if we reached the last sequence else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence } + + currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> + // Notify live mode change to all the listeners attached to the current voice broadcast id + listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) } + + } } private fun getCurrentPlaybackPosition(): Int? { From a3cd0ee790d30bae0f2a5360119f0f3f7edadfce Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 16:34:43 +0100 Subject: [PATCH 228/679] Fix fetch playlist task getting stopped event from other voice broadcast --- .../voicebroadcast/VoiceBroadcastExtensions.kt | 3 +++ .../usecase/GetLiveVoiceBroadcastChunksUseCase.kt | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index fa8033a211..6faec5a262 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -39,6 +39,9 @@ val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 +val VoiceBroadcastEvent.voiceBroadcastId + get() = reference?.eventId + val VoiceBroadcastEvent.isLive get() = content?.isLive.orFalse() diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index d12a329142..16b15b9a77 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -25,6 +25,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase +import im.vector.app.features.voicebroadcast.voiceBroadcastId import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -73,14 +74,15 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( // Observe new timeline events val listener = object : Timeline.Listener { - private var lastEventId: String? = null + private var latestEventId: String? = null private var lastSequence: Int? = null override fun onTimelineUpdated(snapshot: List) { - val newEvents = lastEventId?.let { eventId -> snapshot.subList(0, snapshot.indexOfFirst { it.eventId == eventId }) } ?: snapshot + val latestEventIndex = latestEventId?.let { eventId -> snapshot.indexOfFirst { it.eventId == eventId } } + val newEvents = if (latestEventIndex != null) snapshot.subList(0, latestEventIndex) else snapshot // Detect a potential stopped voice broadcast state event - val stopEvent = newEvents.findStopEvent() + val stopEvent = newEvents.findStopEvent(voiceBroadcast) if (stopEvent != null) { lastSequence = stopEvent.content?.lastChunkSequence } @@ -98,7 +100,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( timeline.dispose() } - lastEventId = snapshot.firstOrNull()?.eventId + latestEventId = snapshot.firstOrNull()?.eventId } } @@ -117,8 +119,8 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( /** * Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state. */ - private fun List.findStopEvent(): VoiceBroadcastEvent? = - this.mapNotNull { it.root.asVoiceBroadcastEvent() } + private fun List.findStopEvent(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? = + this.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeIf { it.voiceBroadcastId == voiceBroadcast.voiceBroadcastId } } .find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } /** From 73d62c944c5d53eedcdee09d6cc2ba3ce7b25380 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 10 Nov 2022 18:13:00 +0100 Subject: [PATCH 229/679] Emit first event on voice broadcast event flow --- .../usecase/GetVoiceBroadcastEventUseCase.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt index 696d300fc3..94eca2b54e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt @@ -21,11 +21,12 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.voiceBroadcastId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onStart import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType @@ -33,7 +34,7 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.unwrap +import org.matrix.android.sdk.flow.mapOptional import timber.log.Timber import javax.inject.Inject @@ -57,10 +58,10 @@ class GetVoiceBroadcastEventUseCase @Inject constructor( else -> { room.flow() .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty())) - .unwrap() - .mapNotNull { it.asVoiceBroadcastEvent() } - .filter { it.reference?.eventId == voiceBroadcast.voiceBroadcastId } - .map { it.toOptional() } + .onStart { emit(latestEvent.root.toOptional()) } + .distinctUntilChanged() + .filter { !it.hasValue() || it.getOrNull()?.asVoiceBroadcastEvent()?.voiceBroadcastId == voiceBroadcast.voiceBroadcastId } + .mapOptional { it.asVoiceBroadcastEvent() } } } } From 44608f080c6fd8777fd9ccad2b6877c0746a313e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 10:24:22 +0100 Subject: [PATCH 230/679] Improve logs --- .../listening/VoiceBroadcastPlayerImpl.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 56c80ddfb1..9199de79cf 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -70,18 +70,26 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override var currentVoiceBroadcast: VoiceBroadcast? = null override var isLiveListening: Boolean = false + @MainThread + set(value) { + if (field != value) { + Timber.w("isLiveListening: $field -> $value") + field = value + onLiveListeningChanged(value) + } + } override var playingState = State.IDLE @MainThread set(value) { if (field != value) { - Timber.w("## VoiceBroadcastPlayer state: $field -> $value") + Timber.w("playingState: $field -> $value") field = value onPlayingStateChanged(value) } } - /** Map voiceBroadcastId to listeners.*/ + /** Map voiceBroadcastId to listeners. */ private val listeners: MutableMap> = mutableMapOf() override fun playOrResume(voiceBroadcast: VoiceBroadcast) { @@ -325,9 +333,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( /** * Update the live listening state according to: - * - the voice broadcast state, - * - the playing state, - * - the potential seek position. + * - the voice broadcast state (started/paused/resumed/stopped), + * - the playing state (IDLE, PLAYING, PAUSED, BUFFERING), + * - the potential seek position (backward/forward). */ private fun updateLiveListeningMode(seekPosition: Int? = null) { isLiveListening = when { @@ -348,11 +356,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // otherwise, stay in live or go in live if we reached the last sequence else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence } + } + private fun onLiveListeningChanged(isLiveListening: Boolean) { currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> // Notify live mode change to all the listeners attached to the current voice broadcast id listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) } - } } From 288fc354878638ca71a658a91122feb2b3daaef0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 10:46:40 +0100 Subject: [PATCH 231/679] Changelog --- changelog.d/7579.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7579.wip diff --git a/changelog.d/7579.wip b/changelog.d/7579.wip new file mode 100644 index 0000000000..08e6c2cdca --- /dev/null +++ b/changelog.d/7579.wip @@ -0,0 +1 @@ +[Voice Broadcast] Improve the live indicator icon rendering in the timeline From 403fd9260ee299f6142a55e4eb993d4e766a80fd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 11:57:05 +0100 Subject: [PATCH 232/679] improve boolean condition --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 9199de79cf..d04b46b842 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -130,7 +130,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) - listener.onLiveModeChanged(if (voiceBroadcast == currentVoiceBroadcast) isLiveListening else false) + listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast && isLiveListening) } override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { From a476544761b427f7388ab5fbbbbf3e0b105a59be Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 14 Nov 2022 12:01:29 +0100 Subject: [PATCH 233/679] Fix some quoted messages having 'null' message bodies (#7532) * Fix some quoted messages having 'null' message bodies --- changelog.d/7530.sdk | 1 + .../room/send/LocalEchoEventFactory.kt | 67 +++-- .../room/send/LocalEchoEventFactoryTests.kt | 241 ++++++++++++++++++ .../sdk/test/fakes/FakeClipboardManager.kt | 37 +++ .../sdk/test/fakes/FakeConnectivityManager.kt | 44 ++++ .../android/sdk/test/fakes/FakeContext.kt | 84 ++++++ .../sdk/test/fakes/FakeNetworkCapabilities.kt | 32 +++ .../session/content/FakeThumbnailExtractor.kt | 24 ++ .../permalinks/FakePermalinkFactory.kt | 24 ++ .../room/send/FakeLocalEchoRepository.kt | 24 ++ .../session/room/send/FakeMarkdownParser.kt | 32 +++ .../room/send/FakeWaveFormSanitizer.kt | 24 ++ .../room/send/pills/FakeTextPillsUtils.kt | 24 ++ 13 files changed, 632 insertions(+), 26 deletions(-) create mode 100644 changelog.d/7530.sdk create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt diff --git a/changelog.d/7530.sdk b/changelog.d/7530.sdk new file mode 100644 index 0000000000..4cea35f44b --- /dev/null +++ b/changelog.d/7530.sdk @@ -0,0 +1 @@ +Fix a bug that caused messages with no formatted text to be quoted as "null". diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 7d8605c2bd..55ba78c2a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -804,20 +804,12 @@ internal class LocalEchoEventFactory @Inject constructor( additionalContent: Content? = null, ): Event { val messageContent = quotedEvent.getLastMessageContent() - val textMsg = if (messageContent is MessageContentWithFormattedBody) { - messageContent.formattedBody - } else { - messageContent?.body - } - val quoteText = legacyRiotQuoteText(textMsg, text) - val quoteFormattedText = "
$textMsg
$formattedText" - + val formattedQuotedText = (messageContent as? MessageContentWithFormattedBody)?.formattedBody + val textContent = createQuoteTextContent(messageContent?.body, formattedQuotedText, text, formattedText, autoMarkdown) return if (rootThreadEventId != null) { createMessageEvent( roomId, - markdownParser - .parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText) - .toThreadTextContent( + textContent.toThreadTextContent( rootThreadEventId = rootThreadEventId, latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), msgType = MessageType.MSGTYPE_TEXT @@ -827,31 +819,54 @@ internal class LocalEchoEventFactory @Inject constructor( } else { createFormattedTextEvent( roomId, - markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText), + textContent, MessageType.MSGTYPE_TEXT, additionalContent, ) } } - private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { - val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() - return buildString { - if (messageParagraphs != null) { - for (i in messageParagraphs.indices) { - if (messageParagraphs[i].isNotBlank()) { - append("> ") - append(messageParagraphs[i]) - } - - if (i != messageParagraphs.lastIndex) { - append("\n\n") - } + private fun createQuoteTextContent( + quotedText: String?, + formattedQuotedText: String?, + text: String, + formattedText: String?, + autoMarkdown: Boolean + ): TextContent { + val currentFormattedText = formattedText ?: if (autoMarkdown) { + val parsed = markdownParser.parse(text, force = true, advanced = true) + // If formattedText == text, formattedText is returned as null + parsed.formattedText ?: parsed.text + } else { + text + } + val processedFormattedQuotedText = formattedQuotedText ?: quotedText + + val plainTextBody = buildString { + val plainMessageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray().orEmpty() + plainMessageParagraphs.forEachIndexed { index, paragraph -> + if (paragraph.isNotBlank()) { + append("> ") + append(paragraph) + } + + if (index != plainMessageParagraphs.lastIndex) { + append("\n\n") } } append("\n\n") - append(myText) + append(text) + } + val formattedTextBody = buildString { + if (!processedFormattedQuotedText.isNullOrBlank()) { + append("
") + append(processedFormattedQuotedText) + append("
") + } + append("
") + append(currentFormattedText) } + return TextContent(plainTextBody, formattedTextBody) } companion object { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt new file mode 100644 index 0000000000..b30428e5e1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import org.amshove.kluent.internal.assertEquals +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody +import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.TextContent +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeContext +import org.matrix.android.sdk.test.fakes.internal.session.content.FakeThumbnailExtractor +import org.matrix.android.sdk.test.fakes.internal.session.permalinks.FakePermalinkFactory +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeLocalEchoRepository +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeMarkdownParser +import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeWaveFormSanitizer +import org.matrix.android.sdk.test.fakes.internal.session.room.send.pills.FakeTextPillsUtils + +@Suppress("MaxLineLength") +class LocalEchoEventFactoryTests { + + companion object { + internal const val A_USER_ID_1 = "@user_1:matrix.org" + internal const val A_ROOM_ID = "!sUeOGZKsBValPTUMax:matrix.org" + internal const val AN_EVENT_ID = "\$vApgexcL8Vfh-WxYKsFKCDooo67ttbjm3TiVKXaWijU" + internal const val AN_EPOCH = 1655210176L + + val A_START_EVENT = Event( + type = EventType.STATE_ROOM_CREATE, + eventId = AN_EVENT_ID, + originServerTs = 1652435922563, + senderId = A_USER_ID_1, + roomId = A_ROOM_ID + ) + } + + private val fakeContext = FakeContext() + private val fakeMarkdownParser = FakeMarkdownParser() + private val fakeTextPillsUtils = FakeTextPillsUtils() + private val fakeThumbnailExtractor = FakeThumbnailExtractor() + private val fakeWaveFormSanitizer = FakeWaveFormSanitizer() + private val fakeLocalEchoRepository = FakeLocalEchoRepository() + private val fakePermalinkFactory = FakePermalinkFactory() + private val fakeClock = FakeClock() + + private val localEchoEventFactory = LocalEchoEventFactory( + context = fakeContext.instance, + userId = A_USER_ID_1, + markdownParser = fakeMarkdownParser.instance, + textPillsUtils = fakeTextPillsUtils.instance, + thumbnailExtractor = fakeThumbnailExtractor.instance, + waveformSanitizer = fakeWaveFormSanitizer.instance, + localEchoRepository = fakeLocalEchoRepository.instance, + permalinkFactory = fakePermalinkFactory.instance, + clock = fakeClock + ) + + @Before + fun setup() { + fakeClock.givenEpoch(AN_EPOCH) + fakeMarkdownParser.givenBoldMarkdown() + } + + @Test + fun `given a null quotedText, when a quote event is created, then the result message should only contain the new text after new lines`() { + val event = createTimelineEvent(null, null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + assertEquals("\n\nText", quotedContent?.body) + assertEquals("
Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given a plain text quoted message, when a quote event is created, then the result message should contain both the quoted and new text`() { + val event = createTimelineEvent("Quoted", null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + assertEquals("> Quoted\n\nText", quotedContent?.body) + assertEquals("
Quoted

Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given a formatted text quoted message, when a quote event is created, then the result message should contain both the formatted quote and new text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = null, + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals("
Quoted

Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody) + } + + @Test + fun `given formatted text quoted message and new message, when a quote event is created, then the result message should contain both the formatted quote and new formatted text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = "Formatted text", + autoMarkdown = false, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Formatted text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given formatted text quoted message and new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new formatted text, not the markdown processed text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "Text", + formattedText = "Formatted text", + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the plain text version + assertEquals("> Quoted\n\nText", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Formatted text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given a formatted text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new processed formatted text`() { + val event = createTimelineEvent("Quoted", "Quoted") + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "**Text**", + formattedText = null, + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the markdown text version + assertEquals("> Quoted\n\n**Text**", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + @Test + fun `given a plain text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should the plain text quote and new processed formatted text`() { + val event = createTimelineEvent("Quoted", null) + val quotedContent = localEchoEventFactory.createQuotedTextEvent( + roomId = A_ROOM_ID, + quotedEvent = event, + text = "**Text**", + formattedText = null, + autoMarkdown = true, + rootThreadEventId = null, + additionalContent = null, + ).content.toModel() + // This still uses the markdown text version + assertEquals("> Quoted\n\n**Text**", quotedContent?.body) + // This one has the formatted one + assertEquals( + "
Quoted

Text", + (quotedContent as? MessageContentWithFormattedBody)?.formattedBody + ) + } + + private fun createTimelineEvent(quotedText: String?, formattedQuotedText: String?): TimelineEvent { + val textContent = quotedText?.let { + TextContent( + quotedText, + formattedQuotedText + ).toMessageTextContent().toContent() + } + return TimelineEvent( + root = A_START_EVENT, + localId = 1234, + eventId = AN_EVENT_ID, + displayIndex = 0, + senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null), + annotations = if (textContent != null) { + EventAnnotationsSummary( + editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList()) + ) + } else null + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt new file mode 100644 index 0000000000..bce8b41aa9 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import android.content.ClipData +import android.content.ClipboardManager +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify + +class FakeClipboardManager { + val instance = mockk() + + fun givenSetPrimaryClip() { + every { instance.setPrimaryClip(any()) } just runs + } + + fun verifySetPrimaryClip(clipData: ClipData) { + verify { instance.setPrimaryClip(clipData) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt new file mode 100644 index 0000000000..5c3a245c51 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeConnectivityManager { + val instance = mockk() + + fun givenNoActiveConnection() { + every { instance.activeNetwork } returns null + } + + fun givenHasActiveConnection() { + val network = mockk() + every { instance.activeNetwork } returns network + + val networkCapabilities = FakeNetworkCapabilities() + networkCapabilities.givenTransports( + NetworkCapabilities.TRANSPORT_CELLULAR, + NetworkCapabilities.TRANSPORT_WIFI, + NetworkCapabilities.TRANSPORT_VPN + ) + every { instance.getNetworkCapabilities(network) } returns networkCapabilities.instance + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt new file mode 100644 index 0000000000..966c6a1bb2 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import android.content.ClipboardManager +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.Uri +import android.os.ParcelFileDescriptor +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import java.io.OutputStream + +class FakeContext( + private val contentResolver: ContentResolver = mockk() +) { + + val instance = mockk() + + init { + every { instance.contentResolver } returns contentResolver + every { instance.applicationContext } returns instance + } + + fun givenFileDescriptor(uri: Uri, mode: String, factory: () -> ParcelFileDescriptor?) { + val fileDescriptor = factory() + every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor + } + + fun givenSafeOutputStreamFor(uri: Uri): OutputStream { + val outputStream = mockk(relaxed = true) + every { contentResolver.openOutputStream(uri, "wt") } returns outputStream + return outputStream + } + + fun givenMissingSafeOutputStreamFor(uri: Uri) { + every { contentResolver.openOutputStream(uri, "wt") } returns null + } + + fun givenNoConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenNoActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } + + fun givenService(name: String, klass: Class, service: T) { + every { instance.getSystemService(name) } returns service + every { instance.getSystemService(klass) } returns service + } + + fun givenHasConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenHasActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } + + fun givenStartActivity(intent: Intent) { + every { instance.startActivity(intent) } just runs + } + + fun givenClipboardManager(): FakeClipboardManager { + val fakeClipboardManager = FakeClipboardManager() + givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance) + return fakeClipboardManager + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt new file mode 100644 index 0000000000..c630b94d47 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeNetworkCapabilities { + val instance = mockk() + + fun givenTransports(vararg type: Int) { + every { instance.hasTransport(any()) } answers { + val input = it.invocation.args.first() as Int + type.contains(input) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt new file mode 100644 index 0000000000..b541d24161 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.internal.session.content + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor + +class FakeThumbnailExtractor { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt new file mode 100644 index 0000000000..3d7e85424e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.internal.session.permalinks + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory + +class FakePermalinkFactory { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt new file mode 100644 index 0000000000..b10d13824b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.internal.session.room.send + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository + +class FakeLocalEchoRepository { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt new file mode 100644 index 0000000000..a27c9284e7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.internal.session.room.send + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.util.TextContent +import org.matrix.android.sdk.internal.session.room.send.MarkdownParser + +class FakeMarkdownParser { + internal val instance = mockk() + fun givenBoldMarkdown() { + every { instance.parse(any(), any(), any()) } answers { + val text = arg(0) + TextContent(text, "${text.replace("*", "")}") + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt new file mode 100644 index 0000000000..052ddf7831 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.internal.session.room.send + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.WaveFormSanitizer + +class FakeWaveFormSanitizer { + internal val instance = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt new file mode 100644 index 0000000000..0d783d6628 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.internal.session.room.send.pills + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils + +class FakeTextPillsUtils { + internal val instance = mockk() +} From f49a8af9dae72b1f79e817aa1953722db2527993 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:05:09 +0000 Subject: [PATCH 234/679] Bump dagger from 2.44 to 2.44.2 Bumps `dagger` from 2.44 to 2.44.2. Updates `hilt-android-gradle-plugin` from 2.44 to 2.44.2 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.44...dagger-2.44.2) Updates `dagger` from 2.44 to 2.44.2 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.44...dagger-2.44.2) Updates `dagger-compiler` from 2.44 to 2.44.2 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.44...dagger-2.44.2) Updates `hilt-android` from 2.44 to 2.44.2 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.44...dagger-2.44.2) Updates `hilt-android-testing` from 2.44 to 2.44.2 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.44...dagger-2.44.2) Updates `hilt-compiler` from 2.44 to 2.44.2 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.44...dagger-2.44.2) --- updated-dependencies: - dependency-name: com.google.dagger:hilt-android-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.google.dagger:dagger dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.google.dagger:dagger-compiler dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.google.dagger:hilt-android dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.google.dagger:hilt-android-testing dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.google.dagger:hilt-compiler dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index dc66de43ea..8fc38cbbab 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -10,7 +10,7 @@ def gradle = "7.3.1" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.7.21" def kotlinCoroutines = "1.6.4" -def dagger = "2.44" +def dagger = "2.44.2" def appDistribution = "16.0.0-beta05" def retrofit = "2.9.0" def markwon = "4.6.2" From b85fcf9a005300ebf0c4116b2692bf0c982b3ca2 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 00:11:16 +0100 Subject: [PATCH 235/679] Remove debounce on player buttons --- .../timeline/item/MessageVoiceBroadcastListeningItem.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 7c7e69f320..e5cb677763 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -60,7 +60,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem private fun bindButtons(holder: Holder) { with(holder) { - playPauseButton.onClick { + playPauseButton.setOnClickListener { if (player.currentVoiceBroadcast == voiceBroadcast) { when (player.playingState) { VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) @@ -72,11 +72,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } - fastBackwardButton.onClick { + fastBackwardButton.setOnClickListener { val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration) callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) } - fastForwardButton.onClick { + fastForwardButton.setOnClickListener { val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration) callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration)) } @@ -163,7 +163,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem player.removeListener(voiceBroadcast, playerListener) playbackTracker.untrack(voiceBroadcast.voiceBroadcastId) with(holder) { - seekBar.onClick(null) + seekBar.setOnSeekBarChangeListener(null) playPauseButton.onClick(null) fastForwardButton.onClick(null) fastBackwardButton.onClick(null) From d9454af63ed8b35a98fa4075fdc1082ef4a99120 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 00:30:55 +0100 Subject: [PATCH 236/679] Stay in live when moving playback position in the same chunk --- .../listening/VoiceBroadcastPlayerImpl.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index d04b46b842..f065ac4e44 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -343,17 +343,21 @@ class VoiceBroadcastPlayerImpl @Inject constructor( !currentVoiceBroadcastEvent?.isLive.orFalse() -> false // the player is stopped or paused playingState == State.IDLE || playingState == State.PAUSED -> false - // the user has sought seekPosition != null -> { val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0) - when { - // backward - seekDirection < 0 -> false - // forward: check if new sequence is the last one - else -> playlist.findByPosition(seekPosition)?.sequence == playlist.lastOrNull()?.sequence + val newSequence = playlist.findByPosition(seekPosition)?.sequence + // the user has sought forward + if (seekDirection >= 0) { + // stay in live or latest sequence reached + isLiveListening || newSequence == playlist.lastOrNull()?.sequence + } + // the user has sought backward + else { + // was in live and stay in the same sequence + isLiveListening && newSequence == playlist.currentSequence } } - // otherwise, stay in live or go in live if we reached the last sequence + // otherwise, stay in live or go in live if we reached the latest sequence else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence } } From dca379b80fe09184cf730151b0125710bb6a16f1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 9 Nov 2022 11:29:12 +0100 Subject: [PATCH 237/679] Persist the playback state of voice messages across different screens --- .../features/home/room/detail/composer/AudioMessageHelper.kt | 4 ++-- .../home/room/detail/composer/MessageComposerViewModel.kt | 2 +- .../detail/timeline/helper/AudioMessagePlaybackTracker.kt | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index eddfe500b3..07d7ad4d0e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -253,8 +253,8 @@ class AudioMessageHelper @Inject constructor( playbackTicker = null } - fun clearTracker() { - playbackTracker.clear() + fun stopTracking() { + playbackTracker.unregisterListeners() } fun stopAllVoiceActions(deleteRecord: Boolean = true): MultiPickerAudioType? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 23d6e71114..a8be2be5e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -960,7 +960,7 @@ class MessageComposerViewModel @AssistedInject constructor( } fun endAllVoiceActions(deleteRecord: Boolean = true) { - audioMessageHelper.clearTracker() + audioMessageHelper.stopTracking() audioMessageHelper.stopAllVoiceActions(deleteRecord) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index 91f27ce5a8..b7b3846a10 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -136,12 +136,11 @@ class AudioMessagePlaybackTracker @Inject constructor() { } } - fun clear() { + fun unregisterListeners() { listeners.forEach { it.value.onUpdate(Listener.State.Idle) } listeners.clear() - states.clear() } companion object { From a73e707f33b492964c272145229c188f001df7f1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 15:19:22 +0100 Subject: [PATCH 238/679] Changelog --- changelog.d/7582.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7582.feature diff --git a/changelog.d/7582.feature b/changelog.d/7582.feature new file mode 100644 index 0000000000..3aae4759ee --- /dev/null +++ b/changelog.d/7582.feature @@ -0,0 +1 @@ +Voice messages - Persist the playback position across different screens From 7349bc90c07235118b2d1a99f4dcd5252f9211fa Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 15:34:07 +0100 Subject: [PATCH 239/679] Pause playback instead of reset when recording a new voice message --- .../home/room/detail/composer/AudioMessageHelper.kt | 2 +- .../timeline/helper/AudioMessagePlaybackTracker.kt | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index 07d7ad4d0e..b5ea528bd7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -66,7 +66,7 @@ class AudioMessageHelper @Inject constructor( fun startRecording(roomId: String) { stopPlayback() - playbackTracker.makeAllPlaybacksIdle() + playbackTracker.pauseAllPlaybacks() amplitudeList.clear() try { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index b7b3846a10..90fd66f9ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -51,15 +51,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { } fun pauseAllPlaybacks() { - listeners.keys.forEach { key -> - pausePlayback(key) - } - } - - fun makeAllPlaybacksIdle() { - listeners.keys.forEach { key -> - setState(key, Listener.State.Idle) - } + listeners.keys.forEach(::pausePlayback) } /** From 361538254b198597d897e0df847e8a7d1917e13b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 14 Nov 2022 17:30:03 +0100 Subject: [PATCH 240/679] Voice Broadcast - Add maximum length for recording --- .../src/main/res/values/strings.xml | 2 + .../MessageVoiceBroadcastRecordingItem.kt | 24 ++++- .../voicebroadcast/VoiceBroadcastConstants.kt | 3 + .../recording/VoiceBroadcastRecorder.kt | 11 ++- .../recording/VoiceBroadcastRecorderQ.kt | 88 +++++++++++++++++-- .../usecase/StartVoiceBroadcastUseCase.kt | 20 +++-- .../usecase/StartVoiceBroadcastUseCaseTest.kt | 3 +- 7 files changed, 132 insertions(+), 19 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 372692770e..e503cb3fe7 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3101,6 +3101,8 @@ You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. + + %1$s left Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index 9bd6fc45ec..39d2d73c68 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -21,10 +21,12 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick +import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView +import org.threeten.bp.Duration @EpoxyModelClass abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem() { @@ -37,11 +39,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } private fun bindVoiceBroadcastItem(holder: Holder) { - if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) { + if (recorder != null && recorder?.recordingState != VoiceBroadcastRecorder.State.Idle) { recorderListener = object : VoiceBroadcastRecorder.Listener { override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { renderRecordingState(holder, state) } + + override fun onRemainingTimeUpdated(remainingTime: Long?) { + renderRemainingTime(holder, remainingTime) + } }.also { recorder?.addListener(it) } } else { renderVoiceBroadcastState(holder) @@ -58,9 +64,19 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem } override fun renderMetadata(holder: Holder) { - with(holder) { - listenersCountMetadata.isVisible = false - remainingTimeMetadata.isVisible = false + holder.listenersCountMetadata.isVisible = false + } + + private fun renderRemainingTime(holder: Holder, remainingTime: Long?) { + if (remainingTime != null) { + val formattedDuration = TextUtils.formatDurationWithUnits( + holder.view.context, + Duration.ofSeconds(remainingTime.coerceAtLeast(0L)) + ) + holder.remainingTimeMetadata.value = holder.view.resources.getString(R.string.voice_broadcast_recording_time_left, formattedDuration) + holder.remainingTimeMetadata.isVisible = true + } else { + holder.remainingTimeMetadata.isVisible = false } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt index 551eaa4dac..11b4f50d2f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastConstants.kt @@ -28,4 +28,7 @@ object VoiceBroadcastConstants { /** Default voice broadcast chunk duration, in seconds. */ const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120 + + /** Maximum length of the voice broadcast in seconds. */ + const val MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS = 14_400 // 4 hours } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt index 8bc33ed769..bc13d1fea8 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt @@ -22,16 +22,23 @@ import java.io.File interface VoiceBroadcastRecorder : VoiceRecorder { + /** The current chunk number. */ val currentSequence: Int - val state: State - fun startRecord(roomId: String, chunkLength: Int) + /** Current state of the recorder. */ + val recordingState: State + + /** Current remaining time of recording, in seconds, if any. */ + val currentRemainingTime: Long? + + fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) fun addListener(listener: Listener) fun removeListener(listener: Listener) interface Listener { fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int) = Unit fun onStateUpdated(state: State) = Unit + fun onRemainingTimeUpdated(remainingTime: Long?) = Unit } enum class State { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index 519f1f24aa..c5408b768b 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -21,9 +21,11 @@ import android.media.MediaRecorder import android.os.Build import androidx.annotation.RequiresApi import im.vector.app.features.voice.AbstractVoiceRecorderQ +import im.vector.lib.core.utils.timer.CountUpTimer import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentAttachmentData import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.TimeUnit @RequiresApi(Build.VERSION_CODES.Q) class VoiceBroadcastRecorderQ( @@ -32,13 +34,21 @@ class VoiceBroadcastRecorderQ( private var maxFileSize = 0L // zero or negative for no limit private var currentRoomId: String? = null + private var currentMaxLength: Int = 0 + override var currentSequence = 0 - override var state = VoiceBroadcastRecorder.State.Idle + override var recordingState = VoiceBroadcastRecorder.State.Idle set(value) { field = value listeners.forEach { it.onStateUpdated(value) } } + override var currentRemainingTime: Long? = null + set(value) { + field = value + listeners.forEach { it.onRemainingTimeUpdated(value) } + } + private val recordingTicker = RecordingTicker() private val listeners = CopyOnWriteArrayList() override val outputFormat = MediaRecorder.OutputFormat.MPEG_4 @@ -58,33 +68,47 @@ class VoiceBroadcastRecorderQ( } } - override fun startRecord(roomId: String, chunkLength: Int) { + override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) { currentRoomId = roomId maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong() + currentMaxLength = maxLength currentSequence = 1 startRecord(roomId) - state = VoiceBroadcastRecorder.State.Recording + recordingState = VoiceBroadcastRecorder.State.Recording + recordingTicker.start() } override fun pauseRecord() { tryOrNull { mediaRecorder?.stop() } mediaRecorder?.reset() + recordingState = VoiceBroadcastRecorder.State.Paused + recordingTicker.pause() notifyOutputFileCreated() - state = VoiceBroadcastRecorder.State.Paused } override fun resumeRecord() { currentSequence++ currentRoomId?.let { startRecord(it) } - state = VoiceBroadcastRecorder.State.Recording + recordingState = VoiceBroadcastRecorder.State.Recording + recordingTicker.resume() } override fun stopRecord() { super.stopRecord() + + // Stop recording + recordingState = VoiceBroadcastRecorder.State.Idle + recordingTicker.stop() notifyOutputFileCreated() + + // Remove listeners listeners.clear() + + // Reset data currentSequence = 0 - state = VoiceBroadcastRecorder.State.Idle + currentMaxLength = 0 + currentRemainingTime = null + currentRoomId = null } override fun release() { @@ -94,7 +118,8 @@ class VoiceBroadcastRecorderQ( override fun addListener(listener: VoiceBroadcastRecorder.Listener) { listeners.add(listener) - listener.onStateUpdated(state) + listener.onStateUpdated(recordingState) + listener.onRemainingTimeUpdated(currentRemainingTime) } override fun removeListener(listener: VoiceBroadcastRecorder.Listener) { @@ -117,4 +142,53 @@ class VoiceBroadcastRecorderQ( nextOutputFile = null } } + + private fun onElapsedTimeUpdated(elapsedTimeMillis: Long) { + currentRemainingTime = if (currentMaxLength > 0 && recordingState != VoiceBroadcastRecorder.State.Idle) { + val currentMaxLengthMillis = TimeUnit.SECONDS.toMillis(currentMaxLength.toLong()) + val remainingTimeMillis = currentMaxLengthMillis - elapsedTimeMillis + TimeUnit.MILLISECONDS.toSeconds(remainingTimeMillis) + } else { + null + } + } + + private inner class RecordingTicker( + private var recordingTicker: CountUpTimer? = null, + ) { + fun start() { + recordingTicker?.stop() + recordingTicker = CountUpTimer().apply { + tickListener = CountUpTimer.TickListener { onTick(elapsedTime()) } + resume() + onTick(elapsedTime()) + } + } + + fun pause() { + recordingTicker?.apply { + pause() + onTick(elapsedTime()) + } + } + + fun resume() { + recordingTicker?.apply { + resume() + onTick(elapsedTime()) + } + } + + fun stop() { + recordingTicker?.apply { + stop() + onTick(elapsedTime()) + recordingTicker = null + } + } + + private fun onTick(elapsedTimeMillis: Long) { + onElapsedTimeUpdated(elapsedTimeMillis) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 85f72c09da..67c7f602b7 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.core.content.FileProvider import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.toContentAttachmentData +import im.vector.app.features.session.coroutineScope import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent @@ -28,6 +29,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -51,6 +53,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( private val context: Context, private val buildMeta: BuildMeta, private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, + private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase, ) { suspend fun execute(roomId: String): Result = runCatching { @@ -64,7 +67,8 @@ class StartVoiceBroadcastUseCase @Inject constructor( private suspend fun startVoiceBroadcast(room: Room) { Timber.d("## StartVoiceBroadcastUseCase: Send new voice broadcast info state event") - val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the length from the room settings + val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the chunk length from the room settings + val maxLength = VoiceBroadcastConstants.MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS // Todo Get the max length from the room settings val eventId = room.stateService().sendStateEvent( eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, stateKey = session.myUserId, @@ -75,16 +79,22 @@ class StartVoiceBroadcastUseCase @Inject constructor( ).toContent() ) - startRecording(room, eventId, chunkLength) + startRecording(room, eventId, chunkLength, maxLength) } - private fun startRecording(room: Room, eventId: String, chunkLength: Int) { + private fun startRecording(room: Room, eventId: String, chunkLength: Int, maxLength: Int) { voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener { override fun onVoiceMessageCreated(file: File, sequence: Int) { sendVoiceFile(room, file, eventId, sequence) } + + override fun onRemainingTimeUpdated(remainingTime: Long?) { + if (remainingTime != null && remainingTime <= 0) { + session.coroutineScope.launch { stopVoiceBroadcastUseCase.execute(room.roomId) } + } + } }) - voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength) + voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength, maxLength) } private fun sendVoiceFile(room: Room, voiceMessageFile: File, referenceEventId: String, sequence: Int) { @@ -127,7 +137,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( @VisibleForTesting fun assertNoOngoingVoiceBroadcast(room: Room) { when { - voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> { + voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> { Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting } diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index ef78f1c80d..5b4076378c 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -60,6 +60,7 @@ class StartVoiceBroadcastUseCaseTest { context = FakeContext().instance, buildMeta = mockk(), getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, + stopVoiceBroadcastUseCase = mockk() ) ) @@ -67,7 +68,7 @@ class StartVoiceBroadcastUseCaseTest { fun setup() { every { fakeRoom.roomId } returns A_ROOM_ID justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) } - every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle + every { fakeVoiceBroadcastRecorder.recordingState } returns VoiceBroadcastRecorder.State.Idle } @Test From c3090fa45a4258e1d17d43c38daee7819affc6fb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 10:43:50 +0100 Subject: [PATCH 241/679] Changelog --- changelog.d/7588.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7588.wip diff --git a/changelog.d/7588.wip b/changelog.d/7588.wip new file mode 100644 index 0000000000..b3fdda55fc --- /dev/null +++ b/changelog.d/7588.wip @@ -0,0 +1 @@ +Voice Broadcast - Add maximum length From 8ea909970dd873b97ae618f2e0e78d9159303820 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 10:47:39 +0100 Subject: [PATCH 242/679] Fix line length --- .../recording/usecase/StartVoiceBroadcastUseCase.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 67c7f602b7..45f622ad92 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -137,7 +137,8 @@ class StartVoiceBroadcastUseCase @Inject constructor( @VisibleForTesting fun assertNoOngoingVoiceBroadcast(room: Room) { when { - voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> { + voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording || + voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> { Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast") throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting } From 3239ec5d1f6e0f676c98533a8e6e464e44208aad Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 15 Nov 2022 10:52:09 +0100 Subject: [PATCH 243/679] replace negation "!" with ".not()" --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index f065ac4e44..5b0e5b2b1c 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -340,7 +340,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun updateLiveListeningMode(seekPosition: Int? = null) { isLiveListening = when { // the current voice broadcast is not live (ended) - !currentVoiceBroadcastEvent?.isLive.orFalse() -> false + currentVoiceBroadcastEvent?.isLive?.not().orFalse() -> false // the player is stopped or paused playingState == State.IDLE || playingState == State.PAUSED -> false seekPosition != null -> { From 7aa0e3350629f5277141c2caeae4a26c35e62c23 Mon Sep 17 00:00:00 2001 From: "Auri B. P" Date: Sun, 13 Nov 2022 19:59:47 +0000 Subject: [PATCH 244/679] Translated using Weblate (Catalan) Currently translated at 99.6% (2534 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ca/ --- library/ui-strings/src/main/res/values-ca/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/ui-strings/src/main/res/values-ca/strings.xml b/library/ui-strings/src/main/res/values-ca/strings.xml index f9d7145b66..7e3e019ee5 100644 --- a/library/ui-strings/src/main/res/values-ca/strings.xml +++ b/library/ui-strings/src/main/res/values-ca/strings.xml @@ -2837,4 +2837,6 @@ Adhesius Galeria Format de text + Enrere 30 segons + Avança 30 segons \ No newline at end of file From 06d621cd3cfb7957c92f40e476cb9936f4ec5036 Mon Sep 17 00:00:00 2001 From: Platon Terekhov Date: Mon, 14 Nov 2022 10:41:31 +0000 Subject: [PATCH 245/679] Translated using Weblate (Russian) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- .../src/main/res/values-ru/strings.xml | 137 ++++++++++++++++-- 1 file changed, 127 insertions(+), 10 deletions(-) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index cdac891840..53609a4944 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -930,7 +930,7 @@ Безопасность Правила push-уведомлений ID приложения: - push_key: + Ключ Push: Отображаемое название приложения: Отображаемое название сессии: Url: @@ -1000,10 +1000,10 @@ Не удалось подключиться к серверу обнаружения Пожалуйста, введите URL сервера обнаружения Сервер обнаружения не имеет условий использования - Параметры обнаружения появятся после добавления электронной почты. + Параметры обнаружения появятся после добавления адреса электронной почты. Параметры поиска появятся после добавления номера телефона. Отключение от сервера обнаружения будет означать, что другие пользователи не смогут обнаружить вас, и вы не сможете приглашать других по электронной почте или по телефону. - Мы отправили вам электронное письмо с подтверждением на %s, проверьте вашу электронную почту и нажмите на ссылку для подтверждения + Мы отправили вам электронное письмо на %s, проверьте вашу электронную почту и нажмите на ссылку для подтверждения Выбранный сервер обнаружения не имеет условий использования. Продолжайте, только если вы доверяете его владельцу Текстовое сообщение отправлено %s. Введите код проверки, который он содержит. В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере обнаружения %1$s. Вам нужно повторно подключиться к %2$s, чтобы прекратить делиться ими. @@ -1102,7 +1102,7 @@ Предупреждение! Смена пароля приведёт к сбросу всех сквозных ключей шифрования во всех ваших сессиях, что сделает зашифрованную историю разговоров нечитаемой. Настройте резервное копирование ключей или экспортируйте ключи от комнаты из другой сессии, прежде чем сбрасывать пароль. Продолжить - Данный электронный ящик не связан ни с одним аккаунтом + Данный адрес электронной почты не связан ни с одним аккаунтом Проверьте свою почту Письмо с подтверждением было отправлено на %1$s. Нажмите на ссылку, чтобы подтвердить свой новый пароль. Как только вы перейдете по ссылке нажмите ниже. @@ -1241,7 +1241,7 @@ %1$s: %2$s %3$s Удалённые сообщения Показывать заглушку на месте удалённых сообщений - Мы отправили письмо для подтверждения на %s, проверьте почту и нажмите на ссылку для подтверждения + Мы отправили письмо на %s, пожалуйста проверьте почту и нажмите на ссылку для подтверждения Код подтверждения неверный. Попробуйте снова после принятия условий обслуживания на вашем домашнем сервере. Похоже, сервер долгое время не отвечает, что может быть вызвано плохим соединением или ошибкой на сервере. Попробуйте снова через некоторое время. @@ -1351,7 +1351,7 @@ Введите адрес сервера, который вы хотите использовать На ваш почтовый ящик будет отправлено письмо для подтверждения установки нового пароля. Я подтвердил свою электронную почту - Установите адрес электронной почты для восстановления вашей учетной записи. Позже вы можете дополнительно разрешить людям, которых вы знаете, обнаружить вас по электронной почте. + Укажите адрес электронной почты для восстановления вашей учетной записи. Потом вы сможете, при желании, разрешить людям, которых вы знаете, обнаружить вас по адресу электронной почты. Введенный код неверен. Пожалуйста, проверьте. Войти с Matrix ID Войти с Matrix ID @@ -1482,7 +1482,7 @@ Незаверенная Эта сессия является доверенной для безопасного обмена сообщениями, так как %1$s (%2$s) проверил(а) его: %1$s (%2$s) вошел(ла), используя новую сессию: - Пока этот пользователь не доверяет этой сессии, сообщения, отправленные в обе стороны, помечаются предупреждениями. Кроме того, вы можете подтвердить сессию вручную. + Пока этот пользователь не доверяет этой сессии, сообщения, отправленные в обе стороны, помечаются предупреждениями. Вы также можете подтвердить эту сессию вручную. Начать перекрестную подпись Сбросить ключи Почти готово! Показывает ли %s галочку\? @@ -1606,7 +1606,7 @@ Эта операция невозможна. Домашний сервер устарел. Пожалуйста, настройте сначала сервер идентификации. Пожалуйста, примите сначала условия сервера идентификации в настройках. - Для вашей приватности, ${app_name} поддерживает отправку адреса электронной почты и номера телефона только в хэшированном виде. + Для вашей приватности, ${app_name} поддерживает отправку адреса электронной почты и номеров телефонов только в хэшированном виде. Привязка не удалась. Текущая взаимосвязь с этим идентификатором отсутствует. Ваш домашний сервер (%1$s) предлагает использовать %2$s для вашего сервера обнаружения @@ -1793,7 +1793,7 @@ Добавить изображение из Тема Название комнаты - Вы дали свое согласие на отправку электронных писем и телефонных номеров на этот сервер обнаружения для обнаружения других пользователей из ваших контактов. + Вы дали свое согласие на отправку адресов электронных почт и телефонных номеров на этот сервер идентификации для обнаружения других пользователей из ваших контактов. Добавить по QR-коду Разрешить доступ к вашим контактам. Чтобы отсканировать QR-код, вам нужно разрешить доступ к камере. @@ -2396,7 +2396,7 @@ Местоположение Вы согласны отправить эту информацию\? Чтобы обнаружить существующие контакты, необходимо отправить контактную информацию (электронную почту и номера телефонов) на сервер обнаружения. Мы хешируем ваши данные перед отправкой для обеспечения конфиденциальности. - Отправить электронные адреса и номера телефонов %s + Отправить адреса электронных почт и номера телефонов %s Ваши контакты приватны. Чтобы обнаружить пользователей из ваших контактов, нам необходимо ваше разрешение на отправку контактной информации на ваш сервер обнаружения. Системные настройки Версии @@ -2831,4 +2831,121 @@ Выбрано %1$d Выбрано %1$d + Войти в полноэкранный режим + Применить форматирование подчёркиванием + Применить форматирование перечёркиванием + Применить форматирование курсивом + Применить форматирование жирным + Пожалуйста удостоверьтесь в том, что вы знаете откуда этот код. При соединении устройств, вы даёте кому-то полный доступ к вашей учётной записи. + Подтвердить + Попробовать снова + Не сходится\? + Вход + Соединение с устройством + Сканировать QR-код + Входите с мобильного устройства\? + Показать QR-код на этом устройстве + Выберите «Сканировать QR-код» + Начните с экрана входа + Выберите «Войти при помощи QR-кода» + Начните с экрана входа + Выберите «Показать QR-код» + Зайдите в Настройки -> Безопасность и Приватность + Откройте приложение с другого устройства + Домашний сервер не поддерживает вход при помощи QR-кода. + Вход был отменён с другого устройства. + Этот QR-код не работает. + Другое устройство должно войти в учётную запись. + Другое устройство уже выполнило вход. + Во время установки безопасной переписки возникла проблема с безопасностью. Одно из следующего является скомпроментированным: Ваш домашний сервер; Ваше интернет-соединение; Ваше устройство; + Запрос не выполнен. + Запрос был отклонён на другом устройстве. + Соединение не было выполнено за нужное время. + Соединение с этим устройством не поддерживается. + Неудачное соединение + Проверьте устройство, с которого вы вошли в учётную запись. На его экране должен появиться код снизу. Подтвердите, что код снизу такой же, как и на том устройстве: + Безопасное соединение установлено + Сканируйте QR-код снизу при помощи устройства, с которого вы вышли с учётной записи. + Используйте устройство, с которого вы вошли в учётную запись, чтобы сканировать QR-код снизу: + Войти при помощи QR-кода + Используйте камеру на этом устройстве, чтобы сканировать QR-код, отображённый на вашем другом устройстве: + Сканировать QR-код + 3 + 2 + 1 + Нажмите слева сверху, чтобы увидеть опцию отзыва. + Чтобы упростить ${app_name}, вкладки теперь опциональные. Управляйте ими при помощи меню справа сверху. + Универсальное безопасное приложение для переписок с командами, друзьями и организациями. Создайте переписку или присоеденитесь к уже существующей, чтобы начать. + Пространства — новый способ групировать комнаты и людей. Добавьте существующую комнату или создайте новую, используя кнопку слева снизу. + Возможность записывать и отправлять голосовые трансляции в ленту комнаты. + Получите лучший надзор и контроль над всеми вашими сессиями. + Подтверждённые сессии есть везде, где вы используете эту учётную запись, после введения вашего пароля или подтверждения вашей личности при помощи другой подтверждённой сессии. +\n +\nЭто значит, что у вас есть все нужные ключи, чтобы разблокировать зашифрованные сообщения и даёте другим пользователям знать, что вы доверяете этой сессии. + Подтверждённые сессии вошли при помощи ваших учётных данных и были подтверждены, либо при помощи вашего безопасного пароля, либо при помощи подтверждения с другого устройства. +\n +\nЭто значит, что на них находятся ключи шифрования для ваших предыдущих сообщений и дают другим пользователям знать, что эти сессии действительно принадлежат вам. + Неподтверждённые сессии — это сессии, которые вошли при помощи ваших учётных данных, но не были подтверждены. +\n +\nВы должны удостовериться, что узнаёте эти сессии, так как они могут быть несанкционированным входом в вашу учётную запись. + Вы можете использовать это устройство для входа с телефона или веб-устройства при помощи QR-кода. Для этого есть два способа: + Войти при помощи QR-кода + Собственные названия сессий помогут вам легче распознать свои девайсы. + + Выйти из %1$d сессии + Выйти из %1$d сессий + Выйти из %1$d сессий + Выйти из %1$d сессий + + Выйти + Выбрать сессии + Фильтр + + Неактивен %1$d+ день (%2$s) + Неактивен %1$d+ дней (%2$s) + Неактивен %1$d+ дня (%2$s) + Неактивен %1$d+ дня (%2$s) + + Подтвердите текущую сессию, чтобы посмотреть её состояние подтверждения. + Неизвестное состояние проверки + Автоматически принимать виджеты Element Call и давать доступ к микрофону/камере + Включить ярлыки разрешений Element Call + Форматирование текста + Начать новую голосовую трансляцию + Вам необходимо иметь нужные разрешения, чтобы делиться местоположением в реальном времени в этой комнате. + У вас нет разрешения делиться местоположением в реальном времени + При приглашении кого-то в зашифрованную комнату, которая делится историей, зашифрованная история будет видимой. + Вы уже записываете голосовую трансляцию. Пожалуйста закончите текущую голосовую трансляцию, чтобы начать новую. + Кто-то другой уже записывает голосовую трансляцию. Подождите пока их голосовая трансляция закончится, чтобы начать новую. + У вас нет необходимых разрешений для начала голосовой трансляции в этой комнате. Свяжитесь с администратором комнаты, чтобы получить разрешения. + Не получилось начать новую голосовую трансляцию + Перемотать вперёд на 30 секунд + Перемотать назад на 30 секунд + Буферизация + Приостановить голосовую трансляцию + Проиграть или продолжить голосовую трансляцию + Остановить запись голосовой трансляции + Приостановить запись голосовой трансляции + Продолжить запись голосовой трансляции + Прямая трансляция + Подлинность этого зашифрованного сообщения не может быть гарантирована на этом устройстве. + Сканировать QR-код + Отправьте ваше первое сообщение, чтобы пригласить %s в переписку + Этот QR-код выглядит неправильно. Пожалуйста, попробуйте подтвердить другим способом. + Вы не сможете получить доступ к истории зашифрованных сообщений. Сбросьте вашу защищённую резевную копию и ключи подтверждения, чтобы начать заново. + Сброс пароля + Выберите новый пароль + %s пришлёт вам ссылку для подтверждения + %s нуждается в подтверждении вашей учётной записи + %s нуждается в подтверждении вашей учётной записи + Связаться + Element Matrix Services (EMS) — надёжная хостинговая служба для быстрой и безопасной связи в режиме реального времени. Узнайте больше на <a href=\"${ftue_ems_url}\">element.io/ems</a> + Открыть список пространств + Включено: + Что-то пошло не так. Пожалуйста, проверьте соединение и попробуйте ещё раз. + Открыть экран инструментов для разработчика + Простите, эта комната не была найдена. +\nПожалуйста, попробуйте снова позже.%s + ⚠ В этой комнате есть неподтверждённые устройства, они не смогут расшифровывать сообщения, отправленные вами. + Дать разрешение \ No newline at end of file From a23299de6445ae32536ac45cd664df10f67367d7 Mon Sep 17 00:00:00 2001 From: Nui Harime Date: Sun, 13 Nov 2022 19:54:46 +0000 Subject: [PATCH 246/679] Translated using Weblate (Russian) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- library/ui-strings/src/main/res/values-ru/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 53609a4944..fe71c0b596 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -2948,4 +2948,5 @@ \nПожалуйста, попробуйте снова позже.%s ⚠ В этой комнате есть неподтверждённые устройства, они не смогут расшифровывать сообщения, отправленные вами. Дать разрешение + Другие пользователи могут найти вас по %s \ No newline at end of file From 37dcb86e907d8cbe1ab982a18b23230aec66b58d Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 14 Nov 2022 18:53:28 +0000 Subject: [PATCH 247/679] Translated using Weblate (Swedish) Currently translated at 100.0% (2542 of 2542 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/ --- .../src/main/res/values-sv/strings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml index eaa327e977..65318096c7 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -2836,4 +2836,20 @@ %1$d vald %1$d valda + Växla fullskärmsläge + Verifierade sessioner är alla ställen där du använder det här kontot efter att ha angett din lösenfras eller bekräftat din identitet med en annan verifierad session. +\n +\nDetta betyder att du har alla nycklar som krävs för att låsa upp dina krypterade meddelanden att bekräfta för andra användare att du litar på den här sessionen. + + Logga ut ur %1$d session + Logga ut ur %1$d sessioner + + Logga ut + Textformatering + Du spelar redan in en röstsändning. Avsluta din nuvarande röstsändning för att starta en ny. + Någon annan spelar redan in en röstsändning. Vänta på att deras röstsändning avslutas för att starta en ny. + Du är inte behörig att starta en ny röstsändning i det här rummet. Kontakta en rumsadministratör för att uppgradera dina behörigheter. + Kan inte starta en ny röstsändning + Spola framåt 30 sekunder + Spola tillbaka 30 sekunder \ No newline at end of file From ade6028a696ad1ee0b8eeacf4aae562b94b0dc86 Mon Sep 17 00:00:00 2001 From: Platon Terekhov Date: Sun, 13 Nov 2022 19:42:55 +0000 Subject: [PATCH 248/679] Translated using Weblate (Russian) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/ru/ --- fastlane/metadata/android/ru-RU/changelogs/40104260.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40104270.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40104280.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40104300.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40104310.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40104320.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40105000.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40105060.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40105070.txt | 2 ++ 9 files changed, 18 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40104260.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40104270.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40104280.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40104300.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40104310.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40104320.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105000.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105060.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105070.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104260.txt b/fastlane/metadata/android/ru-RU/changelogs/40104260.txt new file mode 100644 index 0000000000..b023e07b3d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104260.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Использование UnifiedPush и разрешение пользователям получать push-оповещения без FCM. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104270.txt b/fastlane/metadata/android/ru-RU/changelogs/40104270.txt new file mode 100644 index 0000000000..ff4e5cdf15 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104270.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Исправления различных багов и улучшения стабильности работы. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104280.txt b/fastlane/metadata/android/ru-RU/changelogs/40104280.txt new file mode 100644 index 0000000000..ff4e5cdf15 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104280.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Исправления различных багов и улучшения стабильности работы. +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104300.txt b/fastlane/metadata/android/ru-RU/changelogs/40104300.txt new file mode 100644 index 0000000000..aec45e0348 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104300.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Улучшены вход и регистрация +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104310.txt b/fastlane/metadata/android/ru-RU/changelogs/40104310.txt new file mode 100644 index 0000000000..aec45e0348 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104310.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Улучшены вход и регистрация +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40104320.txt b/fastlane/metadata/android/ru-RU/changelogs/40104320.txt new file mode 100644 index 0000000000..d6c614f22b --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40104320.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Исправления различных багов и улучшения стабильности работы +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105000.txt b/fastlane/metadata/android/ru-RU/changelogs/40105000.txt new file mode 100644 index 0000000000..93ea0aff68 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105000.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: отложённые личные сообщения включены по умолчанию +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105060.txt b/fastlane/metadata/android/ru-RU/changelogs/40105060.txt new file mode 100644 index 0000000000..234d265dd8 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: новый интерфейс для выбора прикреплённых файлов +Полный список изменений: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105070.txt b/fastlane/metadata/android/ru-RU/changelogs/40105070.txt new file mode 100644 index 0000000000..234d265dd8 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: новый интерфейс для выбора прикреплённых файлов +Полный список изменений: https://github.com/vector-im/element-android/releases From 21b92f7d154382c21880952c14a4dfcbf4440438 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Mon, 14 Nov 2022 18:39:55 +0000 Subject: [PATCH 249/679] Translated using Weblate (Swedish) Currently translated at 100.0% (81 of 81 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/sv/ --- fastlane/metadata/android/sv-SE/changelogs/40105060.txt | 2 ++ fastlane/metadata/android/sv-SE/changelogs/40105070.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/sv-SE/changelogs/40105060.txt create mode 100644 fastlane/metadata/android/sv-SE/changelogs/40105070.txt diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105060.txt b/fastlane/metadata/android/sv-SE/changelogs/40105060.txt new file mode 100644 index 0000000000..d64984fcfb --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105060.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: nytt gränssnitt för val av bilaga. +Full ändringslogg: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105070.txt b/fastlane/metadata/android/sv-SE/changelogs/40105070.txt new file mode 100644 index 0000000000..d64984fcfb --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105070.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: nytt gränssnitt för val av bilaga. +Full ändringslogg: https://github.com/vector-im/element-android/releases From e4caf7be8127961488b1d67e9461b0c4d39433c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 23:03:53 +0000 Subject: [PATCH 250/679] Bump barista from 4.2.0 to 4.3.0 Bumps [barista](https://github.com/AdevintaSpain/Barista) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/AdevintaSpain/Barista/releases) - [Commits](https://github.com/AdevintaSpain/Barista/compare/4.2.0...4.3.0) --- updated-dependencies: - dependency-name: com.adevinta.android:barista dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index bff0193509..5005700257 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -406,7 +406,7 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { + androidTestImplementation('com.adevinta.android:barista:4.3.0') { exclude group: 'org.jetbrains.kotlin' } androidTestImplementation libs.mockk.mockkAndroid diff --git a/vector/build.gradle b/vector/build.gradle index 890236422e..afdd589de6 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -325,7 +325,7 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { + androidTestImplementation('com.adevinta.android:barista:4.3.0') { exclude group: 'org.jetbrains.kotlin' } androidTestImplementation libs.mockk.mockkAndroid From 10775ab2f386ec964e14bcbd32a0fece77c7a863 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 16 Nov 2022 13:13:07 +0100 Subject: [PATCH 251/679] Editing: default to `MessageContent.body` when no `formattedBody` is present (#7592) * Editing: default to `MessageContent.body` when no `formattedBody` is present * Update docs --- changelog.d/7574.sdk | 1 + .../android/sdk/api/session/room/timeline/TimelineEvent.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7574.sdk diff --git a/changelog.d/7574.sdk b/changelog.d/7574.sdk new file mode 100644 index 0000000000..3757334138 --- /dev/null +++ b/changelog.d/7574.sdk @@ -0,0 +1 @@ +If message content has no `formattedBody`, default to `body` when editing. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 223acd1b9c..6f4049de36 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -180,11 +180,13 @@ fun TimelineEvent.isRootThread(): Boolean { /** * Get the latest message body, after a possible edition, stripping the reply prefix if necessary. + * @param formatted Indicates whether the formatted HTML body of the message should be retrieved of the plain text one. + * @return If [formatted] is `true`, the HTML body of the message will be retrieved if available. Otherwise, the plain text/markdown version will be returned. */ fun TimelineEvent.getTextEditableContent(formatted: Boolean): String { val lastMessageContent = getLastMessageContent() val lastContentBody = if (formatted && lastMessageContent is MessageContentWithFormattedBody) { - lastMessageContent.formattedBody + lastMessageContent.formattedBody ?: lastMessageContent.body } else { lastMessageContent?.body } ?: return "" From 33b7294bbf7e79a5a4788144eb9befb95b7c0718 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 10:25:32 +0100 Subject: [PATCH 252/679] Update the recipe to speed up the release process. --- .github/ISSUE_TEMPLATE/release.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index b41188a920..a84f4dfd3b 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -24,8 +24,7 @@ body: ### Do the release - - [ ] Make sure `develop` and `main` are up to date (git pull) - - [ ] Checkout develop and create a release with gitflow, branch name `release/1.2.3` + - [ ] Make sure `develop` and `main` are up to date and create a release with gitflow: `git checkout main; git pull; git checkout develop; git pull; git flow release start '1.2.3'` - [ ] Check the crashes from the PlayStore - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` @@ -34,12 +33,12 @@ body: - [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things - [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs - [ ] (optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. - - [ ] Finish release with gitflow, delete the draft PR (if created) - - [ ] Push `main` and the new tag `v1.2.3` to origin - - [ ] Checkout `develop` + - [ ] Finish release with gitflow, delete the draft PR (if created): `git flow release finish '1.2.3'` + - [ ] Push `main` and the new tag `v1.2.3` to origin: `git push origin main; git push origin 'v1.2.3'` + - [ ] Checkout `develop`: `git checkout develop` - [ ] Increase version (versionPatch + 2) in `./vector/build.gradle` - [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle` - - [ ] Commit and push `develop` + - [ ] Commit and push `develop`: `git commit -m 'version++'; git push origin develop` - [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. - [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. - [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.) From f63c6c328fc1a13adbb15da68367373a0cc82752 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Nov 2022 13:19:40 +0300 Subject: [PATCH 253/679] Fix italic text is truncated when bubble mode and markdown is enabled. --- .../im/vector/app/features/html/EventHtmlRenderer.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 9e869ecde1..a5bf6cc184 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -27,6 +27,7 @@ package im.vector.app.features.html import android.content.Context import android.content.res.Resources +import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.Spannable import androidx.core.text.toSpannable @@ -40,6 +41,7 @@ import im.vector.app.features.settings.VectorPreferences import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon import io.noties.markwon.MarkwonPlugin +import io.noties.markwon.MarkwonSpansFactory import io.noties.markwon.PrecomputedFutureTextSetterCompat import io.noties.markwon.ext.latex.JLatexMathPlugin import io.noties.markwon.ext.latex.JLatexMathTheme @@ -50,6 +52,8 @@ import io.noties.markwon.inlineparser.EntityInlineProcessor import io.noties.markwon.inlineparser.HtmlInlineProcessor import io.noties.markwon.inlineparser.MarkwonInlineParser import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin +import me.gujun.android.span.style.CustomTypefaceSpan +import org.commonmark.node.Emphasis import org.commonmark.node.Node import org.commonmark.parser.Parser import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl @@ -123,6 +127,11 @@ class EventHtmlRenderer @Inject constructor( ) ) .usePlugin(object : AbstractMarkwonPlugin() { + override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { + builder.setFactory( + Emphasis::class.java + ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) } + } override fun configureParser(builder: Parser.Builder) { /* Configuring the Markwon block formatting processor. * Default settings are all Markdown blocks. Turn those off. From c788deacf5a3de628f974cf1556479938cc023ea Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Nov 2022 13:26:09 +0300 Subject: [PATCH 254/679] Revert "Fix italic text is truncated when bubble mode and markdown is enabled." This reverts commit f63c6c328fc1a13adbb15da68367373a0cc82752. --- .../im/vector/app/features/html/EventHtmlRenderer.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index a5bf6cc184..9e869ecde1 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -27,7 +27,6 @@ package im.vector.app.features.html import android.content.Context import android.content.res.Resources -import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.Spannable import androidx.core.text.toSpannable @@ -41,7 +40,6 @@ import im.vector.app.features.settings.VectorPreferences import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon import io.noties.markwon.MarkwonPlugin -import io.noties.markwon.MarkwonSpansFactory import io.noties.markwon.PrecomputedFutureTextSetterCompat import io.noties.markwon.ext.latex.JLatexMathPlugin import io.noties.markwon.ext.latex.JLatexMathTheme @@ -52,8 +50,6 @@ import io.noties.markwon.inlineparser.EntityInlineProcessor import io.noties.markwon.inlineparser.HtmlInlineProcessor import io.noties.markwon.inlineparser.MarkwonInlineParser import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin -import me.gujun.android.span.style.CustomTypefaceSpan -import org.commonmark.node.Emphasis import org.commonmark.node.Node import org.commonmark.parser.Parser import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl @@ -127,11 +123,6 @@ class EventHtmlRenderer @Inject constructor( ) ) .usePlugin(object : AbstractMarkwonPlugin() { - override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { - builder.setFactory( - Emphasis::class.java - ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) } - } override fun configureParser(builder: Parser.Builder) { /* Configuring the Markwon block formatting processor. * Default settings are all Markdown blocks. Turn those off. From a31a9ab5219cfab3df6ce9b735253e980cb81631 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Nov 2022 13:32:53 +0300 Subject: [PATCH 255/679] Fix italic text is truncated when bubble mode and markdown is enabled. --- .../im/vector/app/features/html/EventHtmlRenderer.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 9e869ecde1..9a2bdc791e 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -27,6 +27,7 @@ package im.vector.app.features.html import android.content.Context import android.content.res.Resources +import android.graphics.Typeface import android.graphics.drawable.Drawable import android.text.Spannable import androidx.core.text.toSpannable @@ -40,6 +41,7 @@ import im.vector.app.features.settings.VectorPreferences import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon import io.noties.markwon.MarkwonPlugin +import io.noties.markwon.MarkwonSpansFactory import io.noties.markwon.PrecomputedFutureTextSetterCompat import io.noties.markwon.ext.latex.JLatexMathPlugin import io.noties.markwon.ext.latex.JLatexMathTheme @@ -50,6 +52,8 @@ import io.noties.markwon.inlineparser.EntityInlineProcessor import io.noties.markwon.inlineparser.HtmlInlineProcessor import io.noties.markwon.inlineparser.MarkwonInlineParser import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin +import me.gujun.android.span.style.CustomTypefaceSpan +import org.commonmark.node.Emphasis import org.commonmark.node.Node import org.commonmark.parser.Parser import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl @@ -94,6 +98,12 @@ class EventHtmlRenderer @Inject constructor( // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex builder .usePlugin(object : AbstractMarkwonPlugin() { + override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { + builder.setFactory( + Emphasis::class.java + ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) } + } + override fun processMarkdown(markdown: String): String { return markdown .replace(Regex(""".*?""")) { matchResult -> From 5d3228d97bf701821983f133deb3ac56ed646569 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 12:28:58 +0100 Subject: [PATCH 256/679] `toModel` was not catching com.squareup.moshi.JsonDataException properly (discovered when joining a Jitsi conf added as a Widget) --- .../org/matrix/android/sdk/api/session/events/model/Event.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 1f16041b54..6ae585a273 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -53,7 +53,7 @@ inline fun Content?.toModel(catchError: Boolean = true): T? { val moshiAdapter = moshi.adapter(T::class.java) return try { moshiAdapter.fromJsonValue(this) - } catch (e: Exception) { + } catch (e: Throwable) { if (catchError) { Timber.e(e, "To model failed : $e") null From f7c3e62206fcd87ef1f5843fc96129d403527d75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 12:39:08 +0100 Subject: [PATCH 257/679] Changelog for version 1.5.8 --- CHANGES.md | 40 ++++++++++++++++++++++++++++++++++++++++ changelog.d/7418.feature | 1 - changelog.d/7431.bugfix | 1 - changelog.d/7436.feature | 1 - changelog.d/7448.wip | 1 - changelog.d/7450.wip | 1 - changelog.d/7452.feature | 1 - changelog.d/7457.bugfix | 1 - changelog.d/7478.wip | 1 - changelog.d/7485.wip | 1 - changelog.d/7491.bugfix | 1 - changelog.d/7496.feature | 1 - changelog.d/7496.wip | 1 - changelog.d/7501.bugfix | 1 - changelog.d/7502.bugfix | 1 - changelog.d/7509.bugfix | 1 - changelog.d/7512.feature | 1 - changelog.d/7514.sdk | 1 - changelog.d/7519.bugfix | 1 - changelog.d/7530.sdk | 1 - changelog.d/7533.bugfix | 1 - changelog.d/7574.sdk | 1 - changelog.d/7579.wip | 1 - changelog.d/7582.feature | 1 - changelog.d/7588.wip | 1 - 25 files changed, 40 insertions(+), 24 deletions(-) delete mode 100644 changelog.d/7418.feature delete mode 100644 changelog.d/7431.bugfix delete mode 100644 changelog.d/7436.feature delete mode 100644 changelog.d/7448.wip delete mode 100644 changelog.d/7450.wip delete mode 100644 changelog.d/7452.feature delete mode 100644 changelog.d/7457.bugfix delete mode 100644 changelog.d/7478.wip delete mode 100644 changelog.d/7485.wip delete mode 100644 changelog.d/7491.bugfix delete mode 100644 changelog.d/7496.feature delete mode 100644 changelog.d/7496.wip delete mode 100644 changelog.d/7501.bugfix delete mode 100644 changelog.d/7502.bugfix delete mode 100644 changelog.d/7509.bugfix delete mode 100644 changelog.d/7512.feature delete mode 100644 changelog.d/7514.sdk delete mode 100644 changelog.d/7519.bugfix delete mode 100644 changelog.d/7530.sdk delete mode 100644 changelog.d/7533.bugfix delete mode 100644 changelog.d/7574.sdk delete mode 100644 changelog.d/7579.wip delete mode 100644 changelog.d/7582.feature delete mode 100644 changelog.d/7588.wip diff --git a/CHANGES.md b/CHANGES.md index 18bb2480c3..442d3641dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,43 @@ +Changes in Element v1.5.8 (2022-11-17) +====================================== + +Features ✨ +---------- + - [Session manager] Multi-session signout ([#7418](https://github.com/vector-im/element-android/issues/7418)) + - Rich text editor: add full screen mode. ([#7436](https://github.com/vector-im/element-android/issues/7436)) + - [Rich text editor] Add plain text mode ([#7452](https://github.com/vector-im/element-android/issues/7452)) + - Move TypingView inside the timeline items. ([#7496](https://github.com/vector-im/element-android/issues/7496)) + - Push notifications toggle: align implementation for current session ([#7512](https://github.com/vector-im/element-android/issues/7512)) + - Voice messages - Persist the playback position across different screens ([#7582](https://github.com/vector-im/element-android/issues/7582)) + +Bugfixes 🐛 +---------- + - [Voice Broadcast] Do not display the recorder view for a live broadcast started from another session ([#7431](https://github.com/vector-im/element-android/issues/7431)) + - [Session manager] Hide push notification toggle when there is no server support ([#7457](https://github.com/vector-im/element-android/issues/7457)) + - Fix rich text editor textfield not growing to fill parent on full screen. ([#7491](https://github.com/vector-im/element-android/issues/7491)) + - Fix duplicated mention pills in some cases ([#7501](https://github.com/vector-im/element-android/issues/7501)) + - Voice Broadcast - Fix duplicated voice messages in the internal playlist ([#7502](https://github.com/vector-im/element-android/issues/7502)) + - When joining a room, the message composer is displayed once the room is loaded. ([#7509](https://github.com/vector-im/element-android/issues/7509)) + - Voice Broadcast - Fix error on voice messages in unencrypted rooms ([#7519](https://github.com/vector-im/element-android/issues/7519)) + - Fix description of verified sessions ([#7533](https://github.com/vector-im/element-android/issues/7533)) + +In development 🚧 +---------------- + - [Voice Broadcast] Improve timeline items factory and handle bad recording state display ([#7448](https://github.com/vector-im/element-android/issues/7448)) + - [Voice Broadcast] Stop recording when opening the room after an app restart ([#7450](https://github.com/vector-im/element-android/issues/7450)) + - [Voice Broadcast] Improve playlist fetching and player codebase ([#7478](https://github.com/vector-im/element-android/issues/7478)) + - [Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast ([#7485](https://github.com/vector-im/element-android/issues/7485)) + - [Voice Broadcast] Add seekbar in listening tile ([#7496](https://github.com/vector-im/element-android/issues/7496)) + - [Voice Broadcast] Improve the live indicator icon rendering in the timeline ([#7579](https://github.com/vector-im/element-android/issues/7579)) + - Voice Broadcast - Add maximum length ([#7588](https://github.com/vector-im/element-android/issues/7588)) + +SDK API changes ⚠️ +------------------ + - [Metrics] Add `SpannableMetricPlugin` to support spans within transactions. ([#7514](https://github.com/vector-im/element-android/issues/7514)) + - Fix a bug that caused messages with no formatted text to be quoted as "null". ([#7530](https://github.com/vector-im/element-android/issues/7530)) + - If message content has no `formattedBody`, default to `body` when editing. ([#7574](https://github.com/vector-im/element-android/issues/7574)) + + Changes in Element v1.5.7 (2022-11-07) ====================================== diff --git a/changelog.d/7418.feature b/changelog.d/7418.feature deleted file mode 100644 index b68ef700da..0000000000 --- a/changelog.d/7418.feature +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Multi-session signout diff --git a/changelog.d/7431.bugfix b/changelog.d/7431.bugfix deleted file mode 100644 index 681a1e9aa5..0000000000 --- a/changelog.d/7431.bugfix +++ /dev/null @@ -1 +0,0 @@ - [Voice Broadcast] Do not display the recorder view for a live broadcast started from another session \ No newline at end of file diff --git a/changelog.d/7436.feature b/changelog.d/7436.feature deleted file mode 100644 index b038c975e1..0000000000 --- a/changelog.d/7436.feature +++ /dev/null @@ -1 +0,0 @@ -Rich text editor: add full screen mode. diff --git a/changelog.d/7448.wip b/changelog.d/7448.wip deleted file mode 100644 index a99e5bbcfa..0000000000 --- a/changelog.d/7448.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Improve timeline items factory and handle bad recording state display diff --git a/changelog.d/7450.wip b/changelog.d/7450.wip deleted file mode 100644 index de4d3dc5e1..0000000000 --- a/changelog.d/7450.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Stop recording when opening the room after an app restart diff --git a/changelog.d/7452.feature b/changelog.d/7452.feature deleted file mode 100644 index a811f87c84..0000000000 --- a/changelog.d/7452.feature +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Add plain text mode diff --git a/changelog.d/7457.bugfix b/changelog.d/7457.bugfix deleted file mode 100644 index 9dfbc53329..0000000000 --- a/changelog.d/7457.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Hide push notification toggle when there is no server support diff --git a/changelog.d/7478.wip b/changelog.d/7478.wip deleted file mode 100644 index 2e6602b16d..0000000000 --- a/changelog.d/7478.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Improve playlist fetching and player codebase diff --git a/changelog.d/7485.wip b/changelog.d/7485.wip deleted file mode 100644 index 30cab45d9c..0000000000 --- a/changelog.d/7485.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast diff --git a/changelog.d/7491.bugfix b/changelog.d/7491.bugfix deleted file mode 100644 index 1a87bd03bd..0000000000 --- a/changelog.d/7491.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix rich text editor textfield not growing to fill parent on full screen. diff --git a/changelog.d/7496.feature b/changelog.d/7496.feature deleted file mode 100644 index 721164ee06..0000000000 --- a/changelog.d/7496.feature +++ /dev/null @@ -1 +0,0 @@ -Move TypingView inside the timeline items. diff --git a/changelog.d/7496.wip b/changelog.d/7496.wip deleted file mode 100644 index 49d15d084f..0000000000 --- a/changelog.d/7496.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Add seekbar in listening tile diff --git a/changelog.d/7501.bugfix b/changelog.d/7501.bugfix deleted file mode 100644 index b86258d427..0000000000 --- a/changelog.d/7501.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix duplicated mention pills in some cases diff --git a/changelog.d/7502.bugfix b/changelog.d/7502.bugfix deleted file mode 100644 index 8785310498..0000000000 --- a/changelog.d/7502.bugfix +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Fix duplicated voice messages in the internal playlist diff --git a/changelog.d/7509.bugfix b/changelog.d/7509.bugfix deleted file mode 100644 index 93ec812e0e..0000000000 --- a/changelog.d/7509.bugfix +++ /dev/null @@ -1 +0,0 @@ -When joining a room, the message composer is displayed once the room is loaded. diff --git a/changelog.d/7512.feature b/changelog.d/7512.feature deleted file mode 100644 index 00411a75ad..0000000000 --- a/changelog.d/7512.feature +++ /dev/null @@ -1 +0,0 @@ -Push notifications toggle: align implementation for current session diff --git a/changelog.d/7514.sdk b/changelog.d/7514.sdk deleted file mode 100644 index f335156a49..0000000000 --- a/changelog.d/7514.sdk +++ /dev/null @@ -1 +0,0 @@ -[Metrics] Add `SpannableMetricPlugin` to support spans within transactions. diff --git a/changelog.d/7519.bugfix b/changelog.d/7519.bugfix deleted file mode 100644 index c687bded49..0000000000 --- a/changelog.d/7519.bugfix +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Fix error on voice messages in unencrypted rooms diff --git a/changelog.d/7530.sdk b/changelog.d/7530.sdk deleted file mode 100644 index 4cea35f44b..0000000000 --- a/changelog.d/7530.sdk +++ /dev/null @@ -1 +0,0 @@ -Fix a bug that caused messages with no formatted text to be quoted as "null". diff --git a/changelog.d/7533.bugfix b/changelog.d/7533.bugfix deleted file mode 100644 index 5e603ece22..0000000000 --- a/changelog.d/7533.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix description of verified sessions diff --git a/changelog.d/7574.sdk b/changelog.d/7574.sdk deleted file mode 100644 index 3757334138..0000000000 --- a/changelog.d/7574.sdk +++ /dev/null @@ -1 +0,0 @@ -If message content has no `formattedBody`, default to `body` when editing. diff --git a/changelog.d/7579.wip b/changelog.d/7579.wip deleted file mode 100644 index 08e6c2cdca..0000000000 --- a/changelog.d/7579.wip +++ /dev/null @@ -1 +0,0 @@ -[Voice Broadcast] Improve the live indicator icon rendering in the timeline diff --git a/changelog.d/7582.feature b/changelog.d/7582.feature deleted file mode 100644 index 3aae4759ee..0000000000 --- a/changelog.d/7582.feature +++ /dev/null @@ -1 +0,0 @@ -Voice messages - Persist the playback position across different screens diff --git a/changelog.d/7588.wip b/changelog.d/7588.wip deleted file mode 100644 index b3fdda55fc..0000000000 --- a/changelog.d/7588.wip +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Add maximum length From 1b957073d9d7c4bcb34151ea5aff59bb20213942 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 12:41:21 +0100 Subject: [PATCH 258/679] Adding fastlane file --- fastlane/metadata/android/en-US/changelogs/40105080.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40105080.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40105080.txt b/fastlane/metadata/android/en-US/changelogs/40105080.txt new file mode 100644 index 0000000000..f9ca8cdd7c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105080.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/vector-im/element-android/releases From 65d898e3de69b8bbc4c392a42321c0cb19f65449 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 12:57:08 +0100 Subject: [PATCH 259/679] version++ --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index f50b672077..60b0329fbc 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.8\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.10\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index bff0193509..a9b16f8c6c 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 8 +ext.versionPatch = 10 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From ab94b21807b01c9a30d9214fb1191b0a3db8743e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Nov 2022 15:58:42 +0300 Subject: [PATCH 260/679] Fix the place of the span factory. --- changelog.d/5679.bugfix | 1 + .../im/vector/app/features/html/EventHtmlRenderer.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelog.d/5679.bugfix diff --git a/changelog.d/5679.bugfix b/changelog.d/5679.bugfix new file mode 100644 index 0000000000..0394bc3e5d --- /dev/null +++ b/changelog.d/5679.bugfix @@ -0,0 +1 @@ +Fix italic text is truncated when bubble mode and markdown is enabled diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 9a2bdc791e..21fcbffb03 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -98,12 +98,6 @@ class EventHtmlRenderer @Inject constructor( // It needs to be in this specific format: https://noties.io/Markwon/docs/v4/ext-latex builder .usePlugin(object : AbstractMarkwonPlugin() { - override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { - builder.setFactory( - Emphasis::class.java - ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) } - } - override fun processMarkdown(markdown: String): String { return markdown .replace(Regex(""".*?""")) { matchResult -> @@ -133,6 +127,12 @@ class EventHtmlRenderer @Inject constructor( ) ) .usePlugin(object : AbstractMarkwonPlugin() { + override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { + builder.setFactory( + Emphasis::class.java + ) { _, _ -> CustomTypefaceSpan(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)) } + } + override fun configureParser(builder: Parser.Builder) { /* Configuring the Markwon block formatting processor. * Default settings are all Markdown blocks. Turn those off. From 9901a43dc10ceb25663b69472fae81390a1360fb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 17 Nov 2022 17:06:44 +0100 Subject: [PATCH 261/679] Add changelog entry --- changelog.d/7604.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7604.bugfix diff --git a/changelog.d/7604.bugfix b/changelog.d/7604.bugfix new file mode 100644 index 0000000000..0fbee55bce --- /dev/null +++ b/changelog.d/7604.bugfix @@ -0,0 +1 @@ +ANR on session start when sending client info is enabled From 74c945b7f0f338340a06f160476b0c779dc61821 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 17 Nov 2022 17:11:16 +0100 Subject: [PATCH 262/679] Launching the sending of the client info in a dedicated coroutine to avoid ANR on application start --- .../java/im/vector/app/core/di/ActiveSessionHolder.kt | 4 +--- .../core/session/ConfigureAndStartSessionUseCase.kt | 10 +++++++--- .../session/ConfigureAndStartSessionUseCaseTest.kt | 9 +++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 7e4f73e7a5..f1863cfa23 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -111,9 +111,7 @@ class ActiveSessionHolder @Inject constructor( } ?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session -> setActiveSession(session) - runBlocking { - configureAndStartSessionUseCase.execute(session, startSyncing = startSync) - } + configureAndStartSessionUseCase.execute(session, startSyncing = startSync) } } diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index 71863b8642..c47769052c 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -22,7 +22,9 @@ import im.vector.app.core.extensions.startSyncing import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.sync.FilterService import timber.log.Timber @@ -36,7 +38,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, ) { - suspend fun execute(session: Session, startSyncing: Boolean = true) { + fun execute(session: Session, startSyncing: Boolean = true) { Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") session.open() session.filterService().setFilter(FilterService.FilterPreset.ElementFilter) @@ -45,8 +47,10 @@ class ConfigureAndStartSessionUseCase @Inject constructor( } session.pushersService().refreshPushers() webRtcCallManager.checkForProtocolsSupportIfNeeded() - if (vectorPreferences.isClientInfoRecordingEnabled()) { - updateMatrixClientInfoUseCase.execute(session) + session.coroutineScope.launch { + if (vectorPreferences.isClientInfoRecordingEnabled()) { + updateMatrixClientInfoUseCase.execute(session) + } } enableNotificationsSettingUpdater.onSessionsStarted(session) } diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt index 861e59e0f1..760879b69d 100644 --- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -18,6 +18,7 @@ package im.vector.app.core.session import im.vector.app.core.extensions.startSyncing import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase +import im.vector.app.features.session.coroutineScope import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater import im.vector.app.test.fakes.FakeSession @@ -32,6 +33,7 @@ import io.mockk.mockkStatic import io.mockk.runs import io.mockk.unmockkAll import io.mockk.verify +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before @@ -57,6 +59,7 @@ class ConfigureAndStartSessionUseCaseTest { @Before fun setup() { mockkStatic("im.vector.app.core.extensions.SessionKt") + mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt") } @After @@ -68,6 +71,7 @@ class ConfigureAndStartSessionUseCaseTest { fun `given start sync needed and client info recording enabled when execute then it should be configured properly`() = runTest { // Given val fakeSession = givenASession() + every { fakeSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) @@ -75,6 +79,7 @@ class ConfigureAndStartSessionUseCaseTest { // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) + advanceUntilIdle() // Then verify { fakeSession.startSyncing(fakeContext.instance) } @@ -88,6 +93,7 @@ class ConfigureAndStartSessionUseCaseTest { fun `given start sync needed and client info recording disabled when execute then it should be configured properly`() = runTest { // Given val fakeSession = givenASession() + every { fakeSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false) @@ -95,6 +101,7 @@ class ConfigureAndStartSessionUseCaseTest { // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) + advanceUntilIdle() // Then verify { fakeSession.startSyncing(fakeContext.instance) } @@ -108,6 +115,7 @@ class ConfigureAndStartSessionUseCaseTest { fun `given a session and no start sync needed when execute then it should be configured properly`() = runTest { // Given val fakeSession = givenASession() + every { fakeSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) @@ -115,6 +123,7 @@ class ConfigureAndStartSessionUseCaseTest { // When configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false) + advanceUntilIdle() // Then verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) } From 8bf0ec297c915706419160e5450688d9388ae00e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 23:03:30 +0000 Subject: [PATCH 263/679] Bump firebase-appdistribution-gradle from 3.0.3 to 3.1.1 Bumps firebase-appdistribution-gradle from 3.0.3 to 3.1.1. --- updated-dependencies: - dependency-name: com.google.firebase:firebase-appdistribution-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 78cc9abb02..5648acfba0 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ buildscript { classpath libs.gradle.gradlePlugin classpath libs.gradle.kotlinPlugin classpath libs.gradle.hiltPlugin - classpath 'com.google.firebase:firebase-appdistribution-gradle:3.0.3' + classpath 'com.google.firebase:firebase-appdistribution-gradle:3.1.1' classpath 'com.google.gms:google-services:4.3.14' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' From 7417241cd58d8738566304c91b11e918f6a472cf Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 18 Nov 2022 08:57:37 +0100 Subject: [PATCH 264/679] New RTE full screen implementation with BottomSheet (#7578) * RTE full screen editor using custom BottomSheet * Fix formatting menu item dimensions * Fix bug with insets when opening attachment menu * Clear the EditText for plain text mode when a message is sent * Set `MessageComposerMode.Special` as a sealed class * Fix insets issue on landscape * Fix small UI issues with rounded corners * Use simplified icons for full screen and minimise --- changelog.d/7577.feature | 1 + .../src/main/res/values/strings.xml | 3 + .../ui-styles/src/main/res/values/dimens.xml | 1 + .../src/main/res/values/styles_edit_text.xml | 11 +- .../utils/ExpandingBottomSheetBehavior.kt | 791 ++++++++++++++++++ .../JumpToBottomViewVisibilityManager.kt | 10 +- .../home/room/detail/RoomDetailActivity.kt | 7 + .../home/room/detail/TimelineFragment.kt | 46 +- .../detail/composer/MessageComposerAction.kt | 1 - .../composer/MessageComposerFragment.kt | 170 ++-- .../detail/composer/MessageComposerMode.kt | 28 + .../detail/composer/MessageComposerView.kt | 21 +- .../composer/PlainTextComposerLayout.kt | 198 +++-- .../detail/composer/RichTextComposerLayout.kt | 285 +++++-- .../composer/voice/VoiceMessageViews.kt | 6 +- .../bg_composer_rich_bottom_sheet.xml | 5 + .../bg_composer_rich_edit_text_expanded.xml | 13 - ...bg_composer_rich_edit_text_single_line.xml | 13 - .../main/res/drawable/bottomsheet_handle.xml | 6 + .../res/drawable/ic_composer_collapse.xml | 9 + .../res/drawable/ic_composer_full_screen.xml | 14 +- .../drawable/ic_composer_rich_mic_pressed.xml | 17 + .../ic_composer_rich_text_editor_close.xml | 9 + .../ic_composer_rich_text_editor_edit.xml | 12 + .../drawable/ic_composer_rich_text_save.xml | 16 + .../res/drawable/ic_rich_composer_add.xml | 15 + .../res/drawable/ic_rich_composer_send.xml | 12 + .../res/drawable/ic_voice_mic_recording.xml | 10 - .../src/main/res/layout/composer_layout.xml | 346 ++++---- ...composer_layout_constraint_set_compact.xml | 197 ----- ...omposer_layout_constraint_set_expanded.xml | 197 ----- .../res/layout/composer_rich_text_layout.xml | 356 ++++---- ...ich_text_layout_constraint_set_compact.xml | 233 ------ ...ch_text_layout_constraint_set_expanded.xml | 230 ----- ..._text_layout_constraint_set_fullscreen.xml | 234 ------ .../src/main/res/layout/fragment_composer.xml | 7 +- .../src/main/res/layout/fragment_timeline.xml | 356 ++++---- .../layout/fragment_timeline_fullscreen.xml | 258 ------ .../res/layout/view_rich_text_menu_button.xml | 4 +- .../layout/view_voice_message_recorder.xml | 18 +- 40 files changed, 1916 insertions(+), 2250 deletions(-) create mode 100644 changelog.d/7577.feature create mode 100644 vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt create mode 100644 vector/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml delete mode 100644 vector/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml delete mode 100644 vector/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml create mode 100644 vector/src/main/res/drawable/bottomsheet_handle.xml create mode 100644 vector/src/main/res/drawable/ic_composer_collapse.xml create mode 100644 vector/src/main/res/drawable/ic_composer_rich_mic_pressed.xml create mode 100644 vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml create mode 100644 vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml create mode 100644 vector/src/main/res/drawable/ic_composer_rich_text_save.xml create mode 100644 vector/src/main/res/drawable/ic_rich_composer_add.xml create mode 100644 vector/src/main/res/drawable/ic_rich_composer_send.xml delete mode 100644 vector/src/main/res/drawable/ic_voice_mic_recording.xml delete mode 100644 vector/src/main/res/layout/composer_layout_constraint_set_compact.xml delete mode 100644 vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml delete mode 100644 vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml delete mode 100644 vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml delete mode 100644 vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml delete mode 100644 vector/src/main/res/layout/fragment_timeline_fullscreen.xml diff --git a/changelog.d/7577.feature b/changelog.d/7577.feature new file mode 100644 index 0000000000..e21ccb13c0 --- /dev/null +++ b/changelog.d/7577.feature @@ -0,0 +1 @@ +New implementation of the full screen mode for the Rich Text Editor. diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index e503cb3fe7..ab98f7e141 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -1642,7 +1642,10 @@ It looks like you’re trying to connect to another homeserver. Do you want to sign out? Edit + Editing Reply + Replying to %s + Quoting Reply in thread View In Room diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 22c2a3e62c..4c911c9e97 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -49,6 +49,7 @@ 1dp 28dp 14dp + 44dp 28dp 6dp diff --git a/library/ui-styles/src/main/res/values/styles_edit_text.xml b/library/ui-styles/src/main/res/values/styles_edit_text.xml index b640fc49d9..94f4d86160 100644 --- a/library/ui-styles/src/main/res/values/styles_edit_text.xml +++ b/library/ui-styles/src/main/res/values/styles_edit_text.xml @@ -4,7 +4,7 @@ diff --git a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt new file mode 100644 index 0000000000..0474cdea7e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt @@ -0,0 +1,791 @@ +package im.vector.app.core.utils + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.View.MeasureSpec +import android.view.ViewGroup +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.customview.widget.ViewDragHelper +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import timber.log.Timber +import java.lang.ref.WeakReference +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * BottomSheetBehavior that dynamically resizes its contents as it grows or shrinks. + * Most of the nested scrolling and touch events code is the same as in [BottomSheetBehavior], but we couldn't just extend it. + */ +class ExpandingBottomSheetBehavior : CoordinatorLayout.Behavior { + + companion object { + /** Gets a [ExpandingBottomSheetBehavior] from the passed [view] if it exists. */ + @Suppress("UNCHECKED_CAST") + fun from(view: V): ExpandingBottomSheetBehavior? { + val params = view.layoutParams as? CoordinatorLayout.LayoutParams ?: return null + return params.behavior as? ExpandingBottomSheetBehavior + } + } + + /** [Callback] to notify changes in dragging state and position. */ + interface Callback { + /** Called when the dragging state of the BottomSheet changes. */ + fun onStateChanged(state: State) {} + + /** Called when the position of the BottomSheet changes while dragging. */ + fun onSlidePositionChanged(view: View, yPosition: Float) {} + } + + /** Represents the 4 possible states of the BottomSheet. */ + enum class State(val value: Int) { + /** BottomSheet is at min height, collapsed at the bottom. */ + Collapsed(0), + + /** BottomSheet is being dragged by the user. */ + Dragging(1), + + /** BottomSheet has been released after being dragged by the user and is animating to its destination. */ + Settling(2), + + /** BottomSheet is at its max height. */ + Expanded(3); + + /** Returns whether the BottomSheet is being dragged or is settling after being dragged. */ + fun isDraggingOrSettling(): Boolean = this == Dragging || this == Settling + } + + /** Set to true to enable debug logging of sizes and offsets. Defaults to `false`. */ + var enableDebugLogs = false + + /** Current BottomSheet state. Default to [State.Collapsed]. */ + var state: State = State.Collapsed + private set + + /** Whether the BottomSheet can be dragged by the user or not. Defaults to `true`. */ + var isDraggable = true + + /** [Callback] to notify changes in dragging state and position. */ + var callback: Callback? = null + set(value) { + field = value + // Send initial state + value?.onStateChanged(state) + } + + /** Additional top offset in `dps` to add to the BottomSheet so it doesn't fill the whole screen. Defaults to `0`. */ + var topOffset = 0 + set(value) { + field = value + expandedOffset = -1 + } + + /** Whether the BottomSheet should be expanded up to the bottom of any [AppBarLayout] found in the parent [CoordinatorLayout]. Defaults to `false`. */ + var avoidAppBarLayout = false + set(value) { + field = value + expandedOffset = -1 + } + + /** + * Whether to add the [scrimView], a 'shadow layer' that will be displayed while dragging/expanded so it obscures the content below the BottomSheet. + * Defaults to `false`. + */ + var useScrimView = false + + /** Color to use for the [scrimView] shadow layer. */ + var scrimViewColor = 0x60000000 + + /** [View.TRANSLATION_Z] in `dps` to apply to the [scrimView]. Defaults to `0dp`. */ + var scrimViewTranslationZ = 0 + + /** Whether the content view should be layout to the top of the BottomSheet when it's collapsed. Defaults to true. */ + var applyInsetsToContentViewWhenCollapsed = true + + /** Lambda used to calculate a min collapsed when the view using the behavior should have a special 'collapsed' layout. It's null by default. */ + var minCollapsedHeight: (() -> Int)? = null + + // Internal BottomSheet implementation properties + private var ignoreEvents = false + private var touchingScrollingChild = false + + private var lastY: Int = -1 + private var collapsedOffset = -1 + private var expandedOffset = -1 + private var parentWidth = -1 + private var parentHeight = -1 + + private var activePointerId = -1 + + private var lastNestedScrollDy = -1 + private var isNestedScrolled = false + + private var viewRef: WeakReference? = null + private var nestedScrollingChildRef: WeakReference? = null + private var velocityTracker: VelocityTracker? = null + + private var dragHelper: ViewDragHelper? = null + private var scrimView: View? = null + + private val stateSettlingTracker = StateSettlingTracker() + private var prevState: State? = null + + private var insetBottom = 0 + private var insetTop = 0 + private var insetLeft = 0 + private var insetRight = 0 + + private var initialPaddingTop = 0 + private var initialPaddingBottom = 0 + private var initialPaddingLeft = 0 + private var initialPaddingRight = 0 + private val minCollapsedOffset: Int? + get() { + val minHeight = minCollapsedHeight?.invoke() ?: return null + if (minHeight == -1) return null + return parentHeight - minHeight - insetBottom + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor() : super() + + override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean { + parentWidth = parent.width + parentHeight = parent.height + + if (viewRef == null) { + viewRef = WeakReference(child) + setWindowInsetsListener(child) + // Prevents clicking on overlapped items below the BottomSheet + child.isClickable = true + } + + parent.updatePadding(left = insetLeft, right = insetRight) + + ensureViewDragHelper(parent) + + // Top coordinate before this layout pass + val savedTop = child.top + + // Calculate default position of the BottomSheet's children + parent.onLayoutChild(child, layoutDirection) + + // This should optimise calculations when they're not needed + if (state == State.Collapsed) { + calculateCollapsedOffset(child) + } + calculateExpandedOffset(parent) + + // Apply top and bottom insets to contentView if needed + val appBar = findAppBarLayout(parent) + val contentView = parent.children.find { it !== appBar && it !== child && it !== scrimView } + if (applyInsetsToContentViewWhenCollapsed && state == State.Collapsed && contentView != null) { + val topOffset = appBar?.measuredHeight ?: 0 + val bottomOffset = parentHeight - collapsedOffset + insetTop + val params = contentView.layoutParams as CoordinatorLayout.LayoutParams + if (params.bottomMargin != bottomOffset || params.topMargin != topOffset) { + params.topMargin = topOffset + params.bottomMargin = bottomOffset + contentView.layoutParams = params + } + } + + // Add scrimView if needed + if (useScrimView && scrimView == null) { + val scrimView = View(parent.context) + scrimView.setBackgroundColor(scrimViewColor) + scrimView.translationZ = scrimViewTranslationZ * child.resources.displayMetrics.scaledDensity + scrimView.isVisible = false + val params = CoordinatorLayout.LayoutParams( + CoordinatorLayout.LayoutParams.MATCH_PARENT, + CoordinatorLayout.LayoutParams.MATCH_PARENT + ) + scrimView.layoutParams = params + val currentIndex = parent.children.indexOf(child) + parent.addView(scrimView, currentIndex) + this.scrimView = scrimView + } else if (!useScrimView && scrimView != null) { + parent.removeView(scrimView) + scrimView = null + } + + // Apply insets and resize child based on the current State + when (state) { + State.Collapsed -> { + scrimView?.alpha = 0f + val newHeight = parentHeight - collapsedOffset + insetTop + val params = child.layoutParams + if (params.height != newHeight) { + params.height = newHeight + child.layoutParams = params + } + // If the offset is < insetTop it will cover the status bar too + val newOffset = max(insetTop, collapsedOffset - insetTop) + ViewCompat.offsetTopAndBottom(child, newOffset) + log("State: Collapsed | Offset: $newOffset | Height: $newHeight") + } + State.Dragging, State.Settling -> { + val newOffset = savedTop - child.top + val percentage = max(0f, 1f - (newOffset.toFloat() / collapsedOffset.toFloat())) + scrimView?.let { + if (percentage == 0f) { + it.isVisible = false + } else { + it.alpha = percentage + it.isVisible = true + } + } + val params = child.layoutParams + params.height = parentHeight - savedTop + child.layoutParams = params + ViewCompat.offsetTopAndBottom(child, newOffset) + val stateStr = if (state == State.Dragging) "Dragging" else "Settling" + log("State: $stateStr | Offset: $newOffset | Percentage: $percentage") + } + State.Expanded -> { + val params = child.layoutParams + val newHeight = parentHeight - expandedOffset + if (params.height != newHeight) { + params.height = newHeight + child.layoutParams = params + } + ViewCompat.offsetTopAndBottom(child, expandedOffset) + log("State: Expanded | Offset: $expandedOffset | Height: $newHeight") + } + } + + // Find a nested scrolling child to take into account for touch events + if (nestedScrollingChildRef == null) { + nestedScrollingChildRef = findScrollingChild(child)?.let { WeakReference(it) } + } + + return true + } + + // region: Touch events + override fun onInterceptTouchEvent( + parent: CoordinatorLayout, + child: V, + ev: MotionEvent + ): Boolean { + // Almost everything inside here is verbatim to BottomSheetBehavior's onTouchEvent + if (viewRef != null && viewRef?.get() !== child) { + return true + } + val action = ev.actionMasked + + if (action == MotionEvent.ACTION_DOWN) { + resetTouchEventTracking() + } + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker?.addMovement(ev) + + when (action) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + touchingScrollingChild = false + activePointerId = MotionEvent.INVALID_POINTER_ID + if (ignoreEvents) { + ignoreEvents = false + return false + } + } + MotionEvent.ACTION_DOWN -> { + val x = ev.x.toInt() + lastY = ev.y.toInt() + + // Only intercept nested scrolling events here if the view not being moved by the + // ViewDragHelper. + val scroll = nestedScrollingChildRef?.get() + if (state != State.Settling) { + if (scroll != null && parent.isPointInChildBounds(scroll, x, lastY)) { + activePointerId = ev.getPointerId(ev.actionIndex) + touchingScrollingChild = true + } + } + ignoreEvents = (activePointerId == MotionEvent.INVALID_POINTER_ID && + !parent.isPointInChildBounds(child, x, lastY)) + } + else -> Unit + } + + if (!ignoreEvents && isDraggable && dragHelper?.shouldInterceptTouchEvent(ev) == true) { + return true + } + + // If using scrim view, a click on it should collapse the bottom sheet + if (useScrimView && state == State.Expanded && action == MotionEvent.ACTION_DOWN) { + val y = ev.y.toInt() + if (y <= expandedOffset) { + setState(State.Collapsed) + return true + } + } + + // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because + // it is not the top most view of its parent. This is not necessary when the touch event is + // happening over the scrolling content as nested scrolling logic handles that case. + val scroll = nestedScrollingChildRef?.get() + return (action == MotionEvent.ACTION_MOVE && + scroll != null && + !ignoreEvents && + state != State.Dragging && + !parent.isPointInChildBounds(scroll, ev.x.toInt(), ev.y.toInt()) && + dragHelper != null && + abs(lastY - ev.y.toInt()) > (dragHelper?.touchSlop ?: 0)) + } + + override fun onTouchEvent(parent: CoordinatorLayout, child: V, ev: MotionEvent): Boolean { + // Almost everything inside here is verbatim to BottomSheetBehavior's onTouchEvent + val action = ev.actionMasked + if (state == State.Dragging && action == MotionEvent.ACTION_DOWN) { + return true + } + if (shouldHandleDraggingWithHelper()) { + dragHelper?.processTouchEvent(ev) + } + + // Record the velocity + if (action == MotionEvent.ACTION_DOWN) { + resetTouchEventTracking() + } + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker?.addMovement(ev) + + if (shouldHandleDraggingWithHelper() && action == MotionEvent.ACTION_MOVE && !ignoreEvents) { + if (abs(lastY - ev.y.toInt()) > (dragHelper?.touchSlop ?: 0)) { + dragHelper?.captureChildView(child, ev.getPointerId(ev.actionIndex)) + } + } + + return !ignoreEvents + } + + private fun resetTouchEventTracking() { + activePointerId = ViewDragHelper.INVALID_POINTER + velocityTracker?.recycle() + velocityTracker = null + } + // endregion + + override fun onAttachedToLayoutParams(params: CoordinatorLayout.LayoutParams) { + super.onAttachedToLayoutParams(params) + + viewRef = null + dragHelper = null + } + + override fun onDetachedFromLayoutParams() { + super.onDetachedFromLayoutParams() + + viewRef = null + dragHelper = null + } + + // region: Size measuring and utils + private fun calculateCollapsedOffset(child: View) { + val availableSpace = parentHeight - insetTop + child.measure( + MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(availableSpace, MeasureSpec.AT_MOST), + ) + collapsedOffset = parentHeight - child.measuredHeight + insetTop + } + + private fun calculateExpandedOffset(parent: CoordinatorLayout): Int { + expandedOffset = if (avoidAppBarLayout) { + findAppBarLayout(parent)?.measuredHeight ?: 0 + } else { + 0 + } + topOffset + insetTop + return expandedOffset + } + + private fun ensureViewDragHelper(parent: CoordinatorLayout) { + if (dragHelper == null) { + dragHelper = ViewDragHelper.create(parent, dragHelperCallback) + } + } + + private fun findAppBarLayout(view: View): AppBarLayout? { + return when (view) { + is AppBarLayout -> view + is ViewGroup -> view.children.firstNotNullOfOrNull { findAppBarLayout(it) } + else -> null + } + } + + private fun shouldHandleDraggingWithHelper(): Boolean { + return dragHelper != null && (isDraggable || state == State.Dragging) + } + + private fun log(contents: String, vararg args: Any) { + if (!enableDebugLogs) return + Timber.d(contents, args) + } + // endregion + + // region: State and delayed state settling + fun setState(state: State) { + if (state == this.state) { + return + } else if (viewRef?.get() == null) { + setInternalState(state) + } else { + viewRef?.get()?.let { child -> + runAfterLayout(child) { startSettling(child, state, false) } + } + } + } + + private fun setInternalState(state: State) { + if (!this.state.isDraggingOrSettling()) { + prevState = this.state + } + this.state = state + + viewRef?.get()?.requestLayout() + + callback?.onStateChanged(state) + } + + private fun startSettling(child: View, state: State, isReleasingView: Boolean) { + val top = getTopOffsetForState(state) + log("Settling to: $top") + val isSettling = dragHelper?.let { + if (isReleasingView) { + it.settleCapturedViewAt(child.left, top) + } else { + it.smoothSlideViewTo(child, child.left, top) + } + } ?: false + setInternalState(if (isSettling) State.Settling else state) + + if (isSettling) { + stateSettlingTracker.continueSettlingToState(state) + } + } + + private fun runAfterLayout(child: V, runnable: Runnable) { + if (isLayouting(child)) { + child.post(runnable) + } else { + runnable.run() + } + } + + private fun isLayouting(child: V): Boolean { + return child.parent != null && child.parent.isLayoutRequested && ViewCompat.isAttachedToWindow(child) + } + + private fun getTopOffsetForState(state: State): Int { + return when (state) { + State.Collapsed -> minCollapsedOffset ?: collapsedOffset + State.Expanded -> expandedOffset + else -> error("Cannot get offset for state $state") + } + } + // endregion + + // region: Nested scroll + override fun onStartNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + directTargetChild: View, + target: View, + axes: Int, + type: Int + ): Boolean { + lastNestedScrollDy = 0 + isNestedScrolled = false + return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0 + } + + override fun onNestedPreScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + target: View, + dx: Int, + dy: Int, + consumed: IntArray, + type: Int + ) { + if (type == ViewCompat.TYPE_NON_TOUCH) return + val scrollingChild = nestedScrollingChildRef?.get() + if (target != scrollingChild) return + + val currentTop = child.top + val newTop = currentTop - dy + if (dy > 0) { + // Upward scroll + if (newTop < expandedOffset) { + consumed[1] = currentTop - expandedOffset + ViewCompat.offsetTopAndBottom(child, -consumed[1]) + setInternalState(State.Expanded) + } else { + if (!isDraggable) return + + consumed[1] = dy + ViewCompat.offsetTopAndBottom(child, -dy) + setInternalState(State.Dragging) + } + } else if (dy < 0) { + // Scroll downward + if (!target.canScrollVertically(-1)) { + if (newTop <= collapsedOffset) { + if (!isDraggable) return + + consumed[1] = dy + ViewCompat.offsetTopAndBottom(child, -dy) + setInternalState(State.Dragging) + } else { + consumed[1] = currentTop - collapsedOffset + ViewCompat.offsetTopAndBottom(child, -consumed[1]) + setInternalState(State.Collapsed) + } + } + } + lastNestedScrollDy = dy + isNestedScrolled = true + } + + override fun onNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + target: View, + dxConsumed: Int, + dyConsumed: Int, + dxUnconsumed: Int, + dyUnconsumed: Int, + type: Int, + consumed: IntArray + ) { + // Empty to avoid default behaviour + } + + override fun onNestedPreFling( + coordinatorLayout: CoordinatorLayout, + child: V, + target: View, + velocityX: Float, + velocityY: Float + ): Boolean { + return target == nestedScrollingChildRef?.get() && + (state != State.Expanded || super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)) + } + + private fun findScrollingChild(view: View): View? { + return when { + !view.isVisible -> null + ViewCompat.isNestedScrollingEnabled(view) -> view + view is ViewGroup -> { + view.children.firstNotNullOfOrNull { findScrollingChild(it) } + } + else -> null + } + } + // endregion + + // region: Insets + private fun setWindowInsetsListener(view: View) { + // Create a snapshot of the view's padding state. + initialPaddingLeft = view.paddingLeft + initialPaddingTop = view.paddingTop + initialPaddingRight = view.paddingRight + initialPaddingBottom = view.paddingBottom + + // This should only be used to set initial insets and other edge cases where the insets can't be applied using an animation. + var applyInsetsFromAnimation = false + + // This will animated inset changes, making them look a lot better. However, it won't update initial insets. + ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat { + return applyInsets(view, insets) + } + + override fun onEnd(animation: WindowInsetsAnimationCompat) { + applyInsetsFromAnimation = false + view.requestApplyInsets() + } + }) + + ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat -> + if (!applyInsetsFromAnimation) { + applyInsetsFromAnimation = true + applyInsets(view, insets) + } else { + insets + } + } + + // Request to apply insets as soon as the view is attached to a window. + if (ViewCompat.isAttachedToWindow(view)) { + ViewCompat.requestApplyInsets(view) + } else { + view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + v.removeOnAttachStateChangeListener(this) + ViewCompat.requestApplyInsets(v) + } + + override fun onViewDetachedFromWindow(v: View) = Unit + }) + } + } + + private fun applyInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat { + val insetsType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() + val imeInsets = insets.getInsets(insetsType) + insetTop = imeInsets.top + insetBottom = imeInsets.bottom + insetLeft = imeInsets.left + insetRight = imeInsets.right + + val bottomPadding = initialPaddingBottom + insetBottom + view.setPadding(initialPaddingLeft, initialPaddingTop, initialPaddingRight, bottomPadding) + if (state == State.Collapsed) { + val params = view.layoutParams + params.height = CoordinatorLayout.LayoutParams.WRAP_CONTENT + view.layoutParams = params + calculateCollapsedOffset(view) + } + return WindowInsetsCompat.CONSUMED + } + // endregion + + // Used to add dragging animations along with StateSettlingTracker, and set max and min dragging coordinates. + private val dragHelperCallback = object : ViewDragHelper.Callback() { + + override fun tryCaptureView(child: View, pointerId: Int): Boolean { + if (state == State.Dragging) { + return false + } + + if (touchingScrollingChild) { + return false + } + + if (state == State.Expanded && activePointerId == pointerId) { + val scroll = nestedScrollingChildRef?.get() + if (scroll?.canScrollVertically(-1) == true) { + return false + } + } + + return viewRef?.get() == child + } + + override fun onViewDragStateChanged(state: Int) { + if (state == ViewDragHelper.STATE_DRAGGING && isDraggable) { + setInternalState(State.Dragging) + } + } + + override fun onViewPositionChanged( + changedView: View, + left: Int, + top: Int, + dx: Int, + dy: Int + ) { + super.onViewPositionChanged(changedView, left, top, dx, dy) + + val params = changedView.layoutParams + params.height = parentHeight - top + insetBottom + insetTop + changedView.layoutParams = params + + val collapsedOffset = minCollapsedOffset ?: collapsedOffset + val percentage = 1f - (top - insetTop).toFloat() / collapsedOffset.toFloat() + + callback?.onSlidePositionChanged(changedView, percentage) + } + + override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) { + val actualCollapsedOffset = minCollapsedOffset ?: collapsedOffset + val targetState = if (yvel < 0) { + // Moving up + val currentTop = releasedChild.top + + val yPositionPercentage = currentTop * 100f / actualCollapsedOffset + if (yPositionPercentage >= 0.5f) { + State.Expanded + } else { + State.Collapsed + } + } else if (yvel == 0f || abs(xvel) > abs(yvel)) { + // If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity + // being greater than the Y velocity, settle to the nearest correct height. + + val currentTop = releasedChild.top + if (currentTop < actualCollapsedOffset / 2) { + State.Expanded + } else { + State.Collapsed + } + } else { + // Moving down + val currentTop = releasedChild.top + + val yPositionPercentage = currentTop * 100f / actualCollapsedOffset + if (yPositionPercentage >= 0.5f) { + State.Collapsed + } else { + State.Expanded + } + } + startSettling(releasedChild, targetState, true) + } + + override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int { + return child.left + } + + override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { + val collapsed = minCollapsedOffset ?: collapsedOffset + val maxTop = max(top, insetTop) + return min(max(maxTop, expandedOffset), collapsed) + } + + override fun getViewVerticalDragRange(child: View): Int { + return minCollapsedOffset ?: collapsedOffset + } + } + + // Used to set the current State in a delayed way. + private inner class StateSettlingTracker { + private lateinit var targetState: State + private var isContinueSettlingRunnablePosted = false + + private val continueSettlingRunnable: Runnable = Runnable { + isContinueSettlingRunnablePosted = false + if (dragHelper?.continueSettling(true) == true) { + continueSettlingToState(targetState) + } else { + setInternalState(targetState) + } + } + + fun continueSettlingToState(state: State) { + val view = viewRef?.get() ?: return + + this.targetState = state + if (!isContinueSettlingRunnablePosted) { + ViewCompat.postOnAnimation(view, continueSettlingRunnable) + isContinueSettlingRunnablePosted = true + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt index 1368b71ec6..0f7dc251ae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -34,8 +34,6 @@ class JumpToBottomViewVisibilityManager( private val layoutManager: LinearLayoutManager ) { - private var canShowButtonOnScroll = true - init { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -45,7 +43,7 @@ class JumpToBottomViewVisibilityManager( if (scrollingToPast) { jumpToBottomView.hide() - } else if (canShowButtonOnScroll) { + } else { maybeShowJumpToBottomViewVisibility() } } @@ -68,13 +66,7 @@ class JumpToBottomViewVisibilityManager( } } - fun hideAndPreventVisibilityChangesWithScrolling() { - jumpToBottomView.hide() - canShowButtonOnScroll = false - } - private fun maybeShowJumpToBottomViewVisibility() { - canShowButtonOnScroll = true if (layoutManager.findFirstVisibleItemPosition() > 1) { jumpToBottomView.show() } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index ecbea133df..2ed3bf8614 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -18,10 +18,12 @@ package im.vector.app.features.home.room.detail import android.content.Context import android.content.Intent +import android.graphics.Color import android.os.Bundle import android.view.View import android.widget.Toast import androidx.core.view.GravityCompat +import androidx.core.view.WindowCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -98,6 +100,11 @@ class RoomDetailActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // For dealing with insets and status bar background color + WindowCompat.setDecorFitsSystemWindows(window, false) + window.statusBarColor = Color.TRANSPARENT + supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) waitingView = views.waitingView.waitingView val timelineArgs: TimelineArgs = intent?.extras?.getParcelableCompat(EXTRA_ROOM_DETAIL_ARGS) ?: return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index e1392b7580..9bed0aae04 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -35,16 +35,17 @@ import android.widget.TextView import androidx.activity.addCallback import androidx.annotation.StringRes import androidx.appcompat.view.menu.MenuBuilder -import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.net.toUri import androidx.core.text.toSpannable import androidx.core.util.Pair import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.forEach import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper @@ -67,7 +68,6 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelperFactory import im.vector.app.core.epoxy.LayoutManagerStateRestorer -import im.vector.app.core.extensions.animateLayoutChange import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.containsRtLOverride @@ -187,9 +187,7 @@ import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -418,20 +416,12 @@ class TimelineFragment : } } - if (savedInstanceState == null) { - handleSpaceShare() + ViewCompat.setOnApplyWindowInsetsListener(views.coordinatorLayout) { _, insets -> + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars()) + views.appBarLayout.updatePadding(top = imeInsets.top) + views.voiceMessageRecorderContainer.updatePadding(bottom = imeInsets.bottom) + insets } - - views.scrim.setOnClickListener { - messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false)) - } - - messageComposerViewModel.stateFlow.map { it.isFullScreen } - .distinctUntilChanged() - .onEach { isFullScreen -> - toggleFullScreenEditor(isFullScreen) - } - .launchIn(viewLifecycleOwner.lifecycleScope) } private fun setupBackPressHandling() { @@ -1048,13 +1038,7 @@ class TimelineFragment : override fun onLayoutCompleted(state: RecyclerView.State) { super.onLayoutCompleted(state) updateJumpToReadMarkerViewVisibility() - withState(messageComposerViewModel) { composerState -> - if (!composerState.isFullScreen) { - jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() - } else { - jumpToBottomViewVisibilityManager.hideAndPreventVisibilityChangesWithScrolling() - } - } + jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() } }.apply { // For local rooms, pin the view's content to the top edge (the layout is reversed) @@ -1170,7 +1154,6 @@ class TimelineFragment : if (mainState.tombstoneEvent == null) { views.composerContainer.isInvisible = !messageComposerState.isComposerVisible views.voiceMessageRecorderContainer.isVisible = messageComposerState.isVoiceMessageRecorderVisible - when (messageComposerState.canSendMessage) { CanSendStatus.Allowed -> { NotificationAreaView.State.Hidden @@ -2036,19 +2019,6 @@ class TimelineFragment : } } - private fun toggleFullScreenEditor(isFullScreen: Boolean) { - views.composerContainer.animateLayoutChange(200) - - val constraintSet = ConstraintSet() - val constraintSetId = if (isFullScreen) { - R.layout.fragment_timeline_fullscreen - } else { - R.layout.fragment_timeline - } - constraintSet.clone(requireContext(), constraintSetId) - constraintSet.applyTo(views.rootConstraintLayout) - } - /** * Returns true if the current room is a Thread room, false otherwise. */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt index 30437a016d..ffaaa235cf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt @@ -33,7 +33,6 @@ sealed class MessageComposerAction : VectorViewModelAction { data class OnEntersBackground(val composerText: String) : MessageComposerAction() data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction() data class InsertUserDisplayName(val userId: String) : MessageComposerAction() - data class SetFullScreen(val isFullScreen: Boolean) : MessageComposerAction() // Voice Message diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index aaf63d7f41..d551850ff3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -24,7 +24,6 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.text.Spannable -import android.text.format.DateUtils import android.view.KeyEvent import android.view.LayoutInflater import android.view.View @@ -32,10 +31,7 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.Toast -import androidx.annotation.DrawableRes import androidx.annotation.RequiresApi -import androidx.annotation.StringRes -import androidx.core.content.ContextCompat import androidx.core.text.buildSpannedString import androidx.core.view.isGone import androidx.core.view.isInvisible @@ -51,7 +47,6 @@ import com.vanniktech.emoji.EmojiPopup import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.error.fatalError -import im.vector.app.core.extensions.getVectorLastMessageContent import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.glide.GlideApp @@ -59,7 +54,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.BuildMeta -import im.vector.app.core.utils.DimensionConverter +import im.vector.app.core.utils.ExpandingBottomSheetBehavior import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.registerForPermissionsResult @@ -86,14 +81,9 @@ import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAc import im.vector.app.features.home.room.detail.TimelineViewModel import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel -import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider -import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet -import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan -import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.location.LocationSharingMode -import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.poll.PollMode import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.share.SharedData @@ -104,18 +94,9 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import org.commonmark.parser.Parser import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData -import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageFormat -import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.MatrixItem -import org.matrix.android.sdk.api.util.toMatrixItem import reactivecircus.flowbinding.android.view.focusChanges import reactivecircus.flowbinding.android.widget.textChanges import timber.log.Timber @@ -130,12 +111,7 @@ class MessageComposerFragment : VectorBaseFragment(), A @Inject lateinit var autoCompleterFactory: AutoCompleter.Factory @Inject lateinit var avatarRenderer: AvatarRenderer - @Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider - @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer - @Inject lateinit var dimensionConverter: DimensionConverter - @Inject lateinit var imageContentRenderer: ImageContentRenderer @Inject lateinit var shareIntentHandler: ShareIntentHandler - @Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorFeatures: VectorFeatures @Inject lateinit var buildMeta: BuildMeta @@ -147,10 +123,6 @@ class MessageComposerFragment : VectorBaseFragment(), A autoCompleterFactory.create(roomId, isThreadTimeLine()) } - private val pillsPostProcessor by lazy { - pillsPostProcessorFactory.create(roomId) - } - private val emojiPopup: EmojiPopup by lifecycleAwareLazy { createEmojiPopup() } @@ -166,6 +138,7 @@ class MessageComposerFragment : VectorBaseFragment(), A private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView + private var bottomSheetBehavior: ExpandingBottomSheetBehavior? = null private val timelineViewModel: TimelineViewModel by parentFragmentViewModel() private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel() @@ -192,6 +165,7 @@ class MessageComposerFragment : VectorBaseFragment(), A attachmentsHelper = AttachmentsHelper(requireContext(), this, buildMeta).register() + setupBottomSheet() setupComposer() setupEmojiButton() @@ -217,22 +191,15 @@ class MessageComposerFragment : VectorBaseFragment(), A } } - messageComposerViewModel.stateFlow.map { it.isFullScreen } - .distinctUntilChanged() - .onEach { isFullScreen -> - composer.toggleFullScreen(isFullScreen) - } - .launchIn(viewLifecycleOwner.lifecycleScope) - messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend -> if (!canSend.boolean()) { return@onEach } when (mode) { is SendMode.Regular -> renderRegularMode(mode.text.toString()) - is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text.toString()) - is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.action_quote, mode.text.toString()) - is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text.toString()) + is SendMode.Edit -> renderSpecialMode(MessageComposerMode.Edit(mode.timelineEvent, mode.text.toString())) + is SendMode.Quote -> renderSpecialMode(MessageComposerMode.Quote(mode.timelineEvent, mode.text.toString())) + is SendMode.Reply -> renderSpecialMode(MessageComposerMode.Reply(mode.timelineEvent, mode.text.toString())) is SendMode.Voice -> renderVoiceMessageMode(mode.text) } } @@ -242,6 +209,14 @@ class MessageComposerFragment : VectorBaseFragment(), A .onEach { onTypeSelected(it.attachmentType) } .launchIn(lifecycleScope) + messageComposerViewModel.stateFlow.map { it.isFullScreen } + .distinctUntilChanged() + .onEach { isFullScreen -> + val state = if (isFullScreen) ExpandingBottomSheetBehavior.State.Expanded else ExpandingBottomSheetBehavior.State.Collapsed + bottomSheetBehavior?.setState(state) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + if (savedInstanceState == null) { handleShareData() } @@ -280,11 +255,45 @@ class MessageComposerFragment : VectorBaseFragment(), A ) { mainState, messageComposerState, attachmentState -> if (mainState.tombstoneEvent != null) return@withState - composer.setInvisible(!messageComposerState.isComposerVisible) + (composer as? View)?.isInvisible = !messageComposerState.isComposerVisible composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled } + private fun setupBottomSheet() { + val parentView = view?.parent as? View ?: return + bottomSheetBehavior = ExpandingBottomSheetBehavior.from(parentView)?.apply { + applyInsetsToContentViewWhenCollapsed = true + topOffset = 22 + useScrimView = true + scrimViewTranslationZ = 8 + minCollapsedHeight = { + (composer as? RichTextComposerLayout)?.estimateCollapsedHeight() ?: -1 + } + isDraggable = false + callback = object : ExpandingBottomSheetBehavior.Callback { + override fun onStateChanged(state: ExpandingBottomSheetBehavior.State) { + // Dragging is disabled while the composer is collapsed + bottomSheetBehavior?.isDraggable = state != ExpandingBottomSheetBehavior.State.Collapsed + + val setFullScreen = when (state) { + ExpandingBottomSheetBehavior.State.Collapsed -> false + ExpandingBottomSheetBehavior.State.Expanded -> true + else -> return + } + + (composer as? RichTextComposerLayout)?.setFullScreen(setFullScreen) + + messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(setFullScreen)) + } + + override fun onSlidePositionChanged(view: View, yPosition: Float) { + (composer as? RichTextComposerLayout)?.notifyIsBeingDragged(yPosition) + } + } + } + } + private fun setupComposer() { val composerEditText = composer.editText composerEditText.setHint(R.string.room_message_placeholder) @@ -382,8 +391,7 @@ class MessageComposerFragment : VectorBaseFragment(), A return } if (text.isNotBlank()) { - // We collapse ASAP, if not there will be a slight annoying delay - composer.collapse(true) + composer.renderComposerMode(MessageComposerMode.Normal("")) lockSendButton = true if (formattedText != null) { messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, formattedText, false)) @@ -407,66 +415,12 @@ class MessageComposerFragment : VectorBaseFragment(), A private fun renderRegularMode(content: CharSequence) { autoCompleter.exitSpecialMode() - composer.collapse() - composer.setTextIfDifferent(content) - composer.sendButton.contentDescription = getString(R.string.action_send) + composer.renderComposerMode(MessageComposerMode.Normal(content)) } - private fun renderSpecialMode( - event: TimelineEvent, - @DrawableRes iconRes: Int, - @StringRes descriptionRes: Int, - defaultContent: CharSequence, - ) { + private fun renderSpecialMode(mode: MessageComposerMode.Special) { autoCompleter.enterSpecialMode() - // switch to expanded bar - composer.composerRelatedMessageTitle.apply { - text = event.senderInfo.disambiguatedDisplayName - setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@"))) - } - - val messageContent: MessageContent? = event.getVectorLastMessageContent() - val nonFormattedBody = when (messageContent) { - is MessageAudioContent -> getAudioContentBodyText(messageContent) - is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion() - is MessageBeaconInfoContent -> getString(R.string.live_location_description) - else -> messageContent?.body.orEmpty() - } - var formattedBody: CharSequence? = null - if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { - val parser = Parser.builder().build() - val document = parser.parse(messageContent.formattedBody ?: messageContent.body) - formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor) - } - composer.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody) - - // Image Event - val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66)) - val isImageVisible = if (data != null) { - imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, composer.composerRelatedMessageImage) - true - } else { - imageContentRenderer.clear(composer.composerRelatedMessageImage) - false - } - - composer.composerRelatedMessageImage.isVisible = isImageVisible - - composer.replaceFormattedContent(defaultContent) - - composer.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) - composer.sendButton.contentDescription = getString(descriptionRes) - - avatarRenderer.render(event.senderInfo.toMatrixItem(), composer.composerRelatedMessageAvatar) - - composer.expand { - if (isAdded) { - // need to do it here also when not using quick reply - focusComposerAndShowKeyboard() - composer.composerRelatedMessageImage.isVisible = isImageVisible - } - } - focusComposerAndShowKeyboard() + composer.renderComposerMode(mode) } private fun observerUserTyping() { @@ -489,7 +443,7 @@ class MessageComposerFragment : VectorBaseFragment(), A } private fun focusComposerAndShowKeyboard() { - if (composer.isVisible) { + if ((composer as? View)?.isVisible == true) { composer.editText.showKeyboard(andRequestFocus = true) } } @@ -499,7 +453,7 @@ class MessageComposerFragment : VectorBaseFragment(), A composer.sendButton.alpha = 0f composer.sendButton.isVisible = true composer.sendButton.animate().alpha(1f).setDuration(150).start() - } else if (!event.isVisible) { + } else { composer.sendButton.isInvisible = true } } @@ -510,15 +464,6 @@ class MessageComposerFragment : VectorBaseFragment(), A } } - private fun getAudioContentBodyText(messageContent: MessageAudioContent): String { - val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong()) - return if (messageContent.voiceMessageIndicator != null) { - getString(R.string.voice_message_reply_content, formattedDuration) - } else { - getString(R.string.audio_message_reply_content, messageContent.body, formattedDuration) - } - } - private fun createEmojiPopup(): EmojiPopup { return EmojiPopup( rootView = views.root, @@ -840,11 +785,6 @@ class MessageComposerFragment : VectorBaseFragment(), A return displayName } - /** - * Returns the root thread event if we are in a thread room, otherwise returns null. - */ - fun getRootThreadEventId(): String? = withState(timelineViewModel) { it.rootThreadEventId } - /** * Returns true if the current room is a Thread room, false otherwise. */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt new file mode 100644 index 0000000000..a401f04bf5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.composer + +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +sealed interface MessageComposerMode { + data class Normal(val content: CharSequence?) : MessageComposerMode + + sealed class Special(open val event: TimelineEvent, open val defaultContent: CharSequence) : MessageComposerMode + data class Edit(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) + class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) + class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt index b7e0e29679..44fcf22d4a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt @@ -19,35 +19,24 @@ package im.vector.app.features.home.room.detail.composer import android.text.Editable import android.widget.EditText import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView interface MessageComposerView { + companion object { + const val MAX_LINES_WHEN_COLLAPSED = 10 + } + val text: Editable? val formattedText: String? val editText: EditText val emojiButton: ImageButton? val sendButton: ImageButton val attachmentButton: ImageButton - val fullScreenButton: ImageButton? - val composerRelatedMessageTitle: TextView - val composerRelatedMessageContent: TextView - val composerRelatedMessageImage: ImageView - val composerRelatedMessageActionIcon: ImageView - val composerRelatedMessageAvatar: ImageView var callback: Callback? - var isVisible: Boolean - - fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) - fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) fun setTextIfDifferent(text: CharSequence?): Boolean - fun replaceFormattedContent(text: CharSequence) - fun toggleFullScreen(newValue: Boolean) - - fun setInvisible(isInvisible: Boolean) + fun renderComposerMode(mode: MessageComposerMode) } interface Callback : ComposerEditText.Callback { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt index 939a59fcca..8f4dd9b71d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt @@ -19,44 +19,59 @@ package im.vector.app.features.home.room.detail.composer import android.content.Context import android.net.Uri import android.text.Editable +import android.text.format.DateUtils import android.util.AttributeSet -import android.view.ViewGroup import android.widget.EditText import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet +import android.widget.LinearLayout +import androidx.core.content.ContextCompat import androidx.core.text.toSpannable -import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.transition.ChangeBounds -import androidx.transition.Fade -import androidx.transition.Transition -import androidx.transition.TransitionManager -import androidx.transition.TransitionSet +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.animations.SimpleTransitionListener +import im.vector.app.core.extensions.getVectorLastMessageContent import im.vector.app.core.extensions.setTextIfDifferent +import im.vector.app.core.extensions.showKeyboard +import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ComposerLayoutBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider +import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData +import im.vector.app.features.html.EventHtmlRenderer +import im.vector.app.features.html.PillsPostProcessor +import im.vector.app.features.media.ImageContentRenderer +import org.commonmark.parser.Parser +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject /** * Encapsulate the timeline composer UX. */ +@AndroidEntryPoint class PlainTextComposerLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView { +) : LinearLayout(context, attrs, defStyleAttr), MessageComposerView { + + @Inject lateinit var avatarRenderer: AvatarRenderer + @Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider + @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer + @Inject lateinit var dimensionConverter: DimensionConverter + @Inject lateinit var imageContentRenderer: ImageContentRenderer + @Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory private val views: ComposerLayoutBinding override var callback: Callback? = null - private var currentConstraintSetId: Int = -1 - - private val animationDuration = 100L - override val text: Editable? get() = views.composerEditText.text @@ -65,37 +80,23 @@ class PlainTextComposerLayout @JvmOverloads constructor( override val editText: EditText get() = views.composerEditText + @Suppress("RedundantNullableReturnType") override val emojiButton: ImageButton? get() = views.composerEmojiButton override val sendButton: ImageButton get() = views.sendButton - override fun setInvisible(isInvisible: Boolean) { - this.isInvisible = isInvisible - } override val attachmentButton: ImageButton get() = views.attachmentButton - override val fullScreenButton: ImageButton? = null - override val composerRelatedMessageActionIcon: ImageView - get() = views.composerRelatedMessageActionIcon - override val composerRelatedMessageAvatar: ImageView - get() = views.composerRelatedMessageAvatar - override val composerRelatedMessageContent: TextView - get() = views.composerRelatedMessageContent - override val composerRelatedMessageImage: ImageView - get() = views.composerRelatedMessageImage - override val composerRelatedMessageTitle: TextView - get() = views.composerRelatedMessageTitle - override var isVisible: Boolean - get() = views.root.isVisible - set(value) { views.root.isVisible = value } init { inflate(context, R.layout.composer_layout, this) views = ComposerLayoutBinding.bind(this) - collapse(false) + views.composerEditText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED + + collapse() views.composerEditText.callback = object : ComposerEditText.Callback { override fun onRichContentSelected(contentUri: Uri): Boolean { @@ -121,27 +122,15 @@ class PlainTextComposerLayout @JvmOverloads constructor( } } - override fun replaceFormattedContent(text: CharSequence) { - setTextIfDifferent(text) - } - - override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) { - if (currentConstraintSetId == R.layout.composer_layout_constraint_set_compact) { - // ignore we good - return - } - currentConstraintSetId = R.layout.composer_layout_constraint_set_compact - applyNewConstraintSet(animate, transitionComplete) + private fun collapse(transitionComplete: (() -> Unit)? = null) { + views.relatedMessageGroup.isVisible = false + transitionComplete?.invoke() callback?.onExpandOrCompactChange() } - override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) { - if (currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded) { - // ignore we good - return - } - currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded - applyNewConstraintSet(animate, transitionComplete) + private fun expand(transitionComplete: (() -> Unit)? = null) { + views.relatedMessageGroup.isVisible = true + transitionComplete?.invoke() callback?.onExpandOrCompactChange() } @@ -149,35 +138,92 @@ class PlainTextComposerLayout @JvmOverloads constructor( return views.composerEditText.setTextIfDifferent(text) } - override fun toggleFullScreen(newValue: Boolean) { - // Plain text composer has no full screen + override fun renderComposerMode(mode: MessageComposerMode) { + val specialMode = mode as? MessageComposerMode.Special + if (specialMode != null) { + renderSpecialMode(specialMode) + } else if (mode is MessageComposerMode.Normal) { + collapse() + editText.setTextIfDifferent(mode.content) + } + + views.sendButton.apply { + if (mode is MessageComposerMode.Edit) { + contentDescription = resources.getString(R.string.action_save) + setImageResource(R.drawable.ic_composer_rich_text_save) + } else { + contentDescription = resources.getString(R.string.action_send) + setImageResource(R.drawable.ic_rich_composer_send) + } + } } - private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { - // val wasSendButtonInvisible = views.sendButton.isInvisible - if (animate) { - configureAndBeginTransition(transitionComplete) + private fun renderSpecialMode(specialMode: MessageComposerMode.Special) { + val event = specialMode.event + val defaultContent = specialMode.defaultContent + + val iconRes: Int = when (specialMode) { + is MessageComposerMode.Reply -> R.drawable.ic_reply + is MessageComposerMode.Edit -> R.drawable.ic_edit + is MessageComposerMode.Quote -> R.drawable.ic_quote + } + + val pillsPostProcessor = pillsPostProcessorFactory.create(event.roomId) + + // switch to expanded bar + views.composerRelatedMessageTitle.apply { + text = event.senderInfo.disambiguatedDisplayName + setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@"))) + } + + val messageContent: MessageContent? = event.getVectorLastMessageContent() + val nonFormattedBody = when (messageContent) { + is MessageAudioContent -> getAudioContentBodyText(messageContent) + is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion() + is MessageBeaconInfoContent -> resources.getString(R.string.live_location_description) + else -> messageContent?.body.orEmpty() + } + var formattedBody: CharSequence? = null + if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { + val parser = Parser.builder().build() + val document = parser.parse(messageContent.formattedBody ?: messageContent.body) + formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor) } - ConstraintSet().also { - it.clone(context, currentConstraintSetId) - it.applyTo(this) + views.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody) + + // Image Event + val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66)) + val isImageVisible = if (data != null) { + imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, views.composerRelatedMessageImage) + true + } else { + imageContentRenderer.clear(views.composerRelatedMessageImage) + false + } + + views.composerRelatedMessageImage.isVisible = isImageVisible + + views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(context, iconRes)) + + avatarRenderer.render(event.senderInfo.toMatrixItem(), views.composerRelatedMessageAvatar) + + views.composerEditText.setText(defaultContent) + + expand { + // need to do it here also when not using quick reply + if (isVisible) { + showKeyboard(andRequestFocus = true) + } + views.composerRelatedMessageImage.isVisible = isImageVisible } - // Might be updated by view state just after, but avoid blinks - // views.sendButton.isInvisible = wasSendButtonInvisible } - private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) { - val transition = TransitionSet().apply { - ordering = TransitionSet.ORDERING_SEQUENTIAL - addTransition(ChangeBounds()) - addTransition(Fade(Fade.IN)) - duration = animationDuration - addListener(object : SimpleTransitionListener() { - override fun onTransitionEnd(transition: Transition) { - transitionComplete?.invoke() - } - }) + private fun getAudioContentBodyText(messageContent: MessageAudioContent): String { + val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong()) + return if (messageContent.voiceMessageIndicator != null) { + resources.getString(R.string.voice_message_reply_content, formattedDuration) + } else { + resources.getString(R.string.audio_message_reply_content, messageContent.body, formattedDuration) } - TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 2d2a4a8cd2..85f163360f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -16,25 +16,34 @@ package im.vector.app.features.home.room.detail.composer +import android.annotation.SuppressLint import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.graphics.Color import android.text.Editable import android.text.TextWatcher import android.util.AttributeSet +import android.util.TypedValue import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import android.widget.EditText import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView +import android.widget.LinearLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable +import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.google.android.material.shape.MaterialShapeDrawable import im.vector.app.R -import im.vector.app.core.extensions.animateLayoutChange +import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.setTextIfDifferent +import im.vector.app.core.extensions.showKeyboard import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding import io.element.android.wysiwyg.EditorEditText @@ -46,23 +55,22 @@ class RichTextComposerLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView { +) : LinearLayout(context, attrs, defStyleAttr), MessageComposerView { private val views: ComposerRichTextLayoutBinding override var callback: Callback? = null - private var currentConstraintSetId: Int = -1 - private val animationDuration = 100L - private val maxEditTextLinesWhenCollapsed = 12 - - private val isFullScreen: Boolean get() = currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_fullscreen + // There is no need to persist these values since they're always updated by the parent fragment + private var isFullScreen = false + private var hasRelatedMessage = false var isTextFormattingEnabled = true set(value) { if (field == value) return syncEditTexts() field = value + updateTextFieldBorder(isFullScreen) updateEditTextVisibility() } @@ -82,37 +90,94 @@ class RichTextComposerLayout @JvmOverloads constructor( get() = views.sendButton override val attachmentButton: ImageButton get() = views.attachmentButton - override val fullScreenButton: ImageButton? - get() = views.composerFullScreenButton - override val composerRelatedMessageActionIcon: ImageView - get() = views.composerRelatedMessageActionIcon - override val composerRelatedMessageAvatar: ImageView - get() = views.composerRelatedMessageAvatar - override val composerRelatedMessageContent: TextView - get() = views.composerRelatedMessageContent - override val composerRelatedMessageImage: ImageView - get() = views.composerRelatedMessageImage - override val composerRelatedMessageTitle: TextView - get() = views.composerRelatedMessageTitle - override var isVisible: Boolean - get() = views.root.isVisible - set(value) { views.root.isVisible = value } + + // Border of the EditText + private val borderShapeDrawable: MaterialShapeDrawable by lazy { + MaterialShapeDrawable().apply { + val typedData = TypedValue() + val lineColor = context.theme.obtainStyledAttributes(typedData.data, intArrayOf(R.attr.vctr_content_quaternary)) + .getColor(0, 0) + strokeColor = ColorStateList.valueOf(lineColor) + strokeWidth = 1 * resources.displayMetrics.scaledDensity + fillColor = ColorStateList.valueOf(Color.TRANSPARENT) + val cornerSize = resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line) + setCornerSize(cornerSize.toFloat()) + } + } + + fun setFullScreen(isFullScreen: Boolean) { + editText.updateLayoutParams { + height = if (isFullScreen) 0 else ViewGroup.LayoutParams.WRAP_CONTENT + } + + updateTextFieldBorder(isFullScreen) + updateEditTextVisibility() + + updateEditTextFullScreenState(views.richTextComposerEditText, isFullScreen) + updateEditTextFullScreenState(views.plainTextComposerEditText, isFullScreen) + + views.composerFullScreenButton.setImageResource( + if (isFullScreen) R.drawable.ic_composer_collapse else R.drawable.ic_composer_full_screen + ) + + views.bottomSheetHandle.isVisible = isFullScreen + if (isFullScreen) { + editText.showKeyboard(true) + } else { + editText.hideKeyboard() + } + this.isFullScreen = isFullScreen + } + + fun notifyIsBeingDragged(percentage: Float) { + // Calculate a new shape for the border according to the position in screen + val isSingleLine = editText.lineCount == 1 + val cornerSize = if (!isSingleLine || hasRelatedMessage) { + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded).toFloat() + } else { + val multilineCornerSize = resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded) + val singleLineCornerSize = resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line) + val diff = singleLineCornerSize - multilineCornerSize + multilineCornerSize + diff * (1 - percentage) + } + if (cornerSize != borderShapeDrawable.bottomLeftCornerResolvedSize) { + borderShapeDrawable.setCornerSize(cornerSize) + } + + // Change maxLines while dragging, this should improve the smoothness of animations + val maxLines = if (percentage > 0.25f) { + Int.MAX_VALUE + } else { + MessageComposerView.MAX_LINES_WHEN_COLLAPSED + } + views.richTextComposerEditText.maxLines = maxLines + views.plainTextComposerEditText.maxLines = maxLines + + views.bottomSheetHandle.isVisible = true + } init { inflate(context, R.layout.composer_rich_text_layout, this) views = ComposerRichTextLayoutBinding.bind(this) - collapse(false) + // Workaround to avoid cut-off text caused by padding in scrolled TextView (there is no clipToPadding). + // In TextView, clipTop = padding, but also clipTop -= shadowRadius. So if we set the shadowRadius to padding, they cancel each other + views.richTextComposerEditText.setShadowLayer(views.richTextComposerEditText.paddingBottom.toFloat(), 0f, 0f, 0) + views.plainTextComposerEditText.setShadowLayer(views.richTextComposerEditText.paddingBottom.toFloat(), 0f, 0f, 0) + + renderComposerMode(MessageComposerMode.Normal(null)) views.richTextComposerEditText.addTextChangedListener( - TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() }) + TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder(isFullScreen) }) ) views.plainTextComposerEditText.addTextChangedListener( - TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() }) + TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder(isFullScreen) }) ) - views.composerRelatedMessageCloseButton.setOnClickListener { - collapse() + disallowParentInterceptTouchEvent(views.richTextComposerEditText) + disallowParentInterceptTouchEvent(views.plainTextComposerEditText) + + views.composerModeCloseView.setOnClickListener { callback?.onCloseRelatedMessage() } @@ -125,11 +190,19 @@ class RichTextComposerLayout @JvmOverloads constructor( callback?.onAddAttachment() } - views.composerFullScreenButton.setOnClickListener { - callback?.onFullScreenModeChanged() + views.composerFullScreenButton.apply { + // There's no point in having full screen in landscape since there's almost no vertical space + isInvisible = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + setOnClickListener { + callback?.onFullScreenModeChanged() + } } + views.composerEditTextOuterBorder.background = borderShapeDrawable + setupRichTextMenu() + + updateTextFieldBorder(isFullScreen) } private fun setupRichTextMenu() { @@ -147,6 +220,21 @@ class RichTextComposerLayout @JvmOverloads constructor( } } + @SuppressLint("ClickableViewAccessibility") + private fun disallowParentInterceptTouchEvent(view: View) { + view.setOnTouchListener { v, event -> + if (v.hasFocus()) { + v.parent?.requestDisallowInterceptTouchEvent(true) + val action = event.actionMasked + if (action == MotionEvent.ACTION_SCROLL) { + v.parent?.requestDisallowInterceptTouchEvent(false) + return@setOnTouchListener true + } + } + false + } + } + override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -197,84 +285,99 @@ class RichTextComposerLayout @JvmOverloads constructor( button.isSelected = menuState.reversedActions.contains(action) } - private fun updateTextFieldBorder() { - val isExpanded = editText.editableText.lines().count() > 1 - val borderResource = if (isExpanded || isFullScreen) { - R.drawable.bg_composer_rich_edit_text_expanded - } else { - R.drawable.bg_composer_rich_edit_text_single_line - } - views.composerEditTextOuterBorder.setBackgroundResource(borderResource) - } - - override fun replaceFormattedContent(text: CharSequence) { - views.richTextComposerEditText.setHtml(text.toString()) + fun estimateCollapsedHeight(): Int { + val editText = this.editText + val originalLines = editText.maxLines + val originalParamsHeight = editText.layoutParams.height + editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED + editText.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED, + ) + val result = measuredHeight + editText.layoutParams.height = originalParamsHeight + editText.maxLines = originalLines + return result } - override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) { - if (currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_compact) { - // ignore we good - return - } - currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact - applyNewConstraintSet(animate, transitionComplete) - updateEditTextVisibility() + private fun updateTextFieldBorder(isFullScreen: Boolean) { + val isMultiline = editText.editableText.lines().count() > 1 || isFullScreen || hasRelatedMessage + val cornerSize = if (isMultiline) { + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded) + } else { + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line) + }.toFloat() + borderShapeDrawable.setCornerSize(cornerSize) } - override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) { - if (currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_expanded) { - // ignore we good - return - } - currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded - applyNewConstraintSet(animate, transitionComplete) - updateEditTextVisibility() + private fun replaceFormattedContent(text: CharSequence) { + views.richTextComposerEditText.setHtml(text.toString()) + updateTextFieldBorder(isFullScreen) } override fun setTextIfDifferent(text: CharSequence?): Boolean { - return editText.setTextIfDifferent(text) - } - - override fun toggleFullScreen(newValue: Boolean) { - val constraintSetId = if (newValue) R.layout.composer_rich_text_layout_constraint_set_fullscreen else currentConstraintSetId - ConstraintSet().also { - it.clone(context, constraintSetId) - it.applyTo(this) - } - - updateTextFieldBorder() - updateEditTextVisibility() - - updateEditTextFullScreenState(views.richTextComposerEditText, newValue) - updateEditTextFullScreenState(views.plainTextComposerEditText, newValue) + val result = editText.setTextIfDifferent(text) + updateTextFieldBorder(isFullScreen) + return result } private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) { if (isFullScreen) { editText.maxLines = Int.MAX_VALUE - // This is a workaround to fix incorrect scroll position when maximised - post { editText.requestLayout() } } else { - editText.maxLines = maxEditTextLinesWhenCollapsed + editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED } } - private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { - // val wasSendButtonInvisible = views.sendButton.isInvisible - if (animate) { - animateLayoutChange(animationDuration, transitionComplete) + override fun renderComposerMode(mode: MessageComposerMode) { + if (mode is MessageComposerMode.Special) { + views.composerModeGroup.isVisible = true + replaceFormattedContent(mode.defaultContent) + hasRelatedMessage = true + editText.showKeyboard(andRequestFocus = true) + } else { + views.composerModeGroup.isGone = true + (mode as? MessageComposerMode.Normal)?.content?.let { text -> + if (isTextFormattingEnabled) { + replaceFormattedContent(text) + } else { + views.plainTextComposerEditText.setText(text) + } + } + views.sendButton.contentDescription = resources.getString(R.string.action_send) + hasRelatedMessage = false } - ConstraintSet().also { - it.clone(context, currentConstraintSetId) - it.applyTo(this) + + views.sendButton.apply { + if (mode is MessageComposerMode.Edit) { + contentDescription = resources.getString(R.string.action_save) + setImageResource(R.drawable.ic_composer_rich_text_save) + } else { + contentDescription = resources.getString(R.string.action_send) + setImageResource(R.drawable.ic_rich_composer_send) + } } - // Might be updated by view state just after, but avoid blinks - // views.sendButton.isInvisible = wasSendButtonInvisible - } + updateTextFieldBorder(isFullScreen) - override fun setInvisible(isInvisible: Boolean) { - this.isInvisible = isInvisible + when (mode) { + is MessageComposerMode.Edit -> { + views.composerModeTitleView.setText(R.string.editing) + views.composerModeIconView.setImageResource(R.drawable.ic_composer_rich_text_editor_edit) + } + is MessageComposerMode.Quote -> { + views.composerModeTitleView.setText(R.string.quoting) + views.composerModeIconView.setImageResource(R.drawable.ic_quote) + } + is MessageComposerMode.Reply -> { + val senderInfo = mode.event.senderInfo + val userName = senderInfo.displayName ?: senderInfo.disambiguatedDisplayName + views.composerModeTitleView.text = resources.getString(R.string.replying_to, userName) + views.composerModeIconView.setImageResource(R.drawable.ic_reply) + } + else -> Unit + } } private class TextChangeListener( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt index cd41219371..ca31c53bb3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt @@ -147,7 +147,8 @@ class VoiceMessageViews( } fun showRecordingViews() { - views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic_recording) + views.voiceMessageBackgroundView.isVisible = true + views.voiceMessageMicButton.setImageResource(R.drawable.ic_composer_rich_mic_pressed) views.voiceMessageMicButton.setAttributeTintedBackground(R.drawable.circle_with_halo, R.attr.colorPrimary) views.voiceMessageMicButton.updateLayoutParams { setMargins(0, 0, 0, 0) @@ -172,6 +173,7 @@ class VoiceMessageViews( fun hideRecordingViews(recordingState: RecordingUiState) { // We need to animate the lock image first + views.voiceMessageBackgroundView.isVisible = false if (recordingState !is RecordingUiState.Locked) { views.voiceMessageLockImage.isVisible = false views.voiceMessageLockImage.animate().translationY(0f).start() @@ -278,6 +280,7 @@ class VoiceMessageViews( fun showDraftViews() { hideRecordingViews(RecordingUiState.Idle) + views.voiceMessageBackgroundView.isVisible = true views.voiceMessageMicButton.isVisible = false views.voiceMessageSendButton.isVisible = true views.voiceMessagePlaybackLayout.isVisible = true @@ -288,6 +291,7 @@ class VoiceMessageViews( fun showRecordingLockedViews(recordingState: RecordingUiState) { hideRecordingViews(recordingState) + views.voiceMessageBackgroundView.isVisible = true views.voiceMessagePlaybackLayout.isVisible = true views.voiceMessagePlaybackTimerIndicator.isVisible = true views.voicePlaybackControlButton.isVisible = false diff --git a/vector/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml b/vector/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml new file mode 100644 index 0000000000..47364373f7 --- /dev/null +++ b/vector/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/vector/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml b/vector/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml deleted file mode 100644 index 26d997e7db..0000000000 --- a/vector/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/vector/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml b/vector/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml deleted file mode 100644 index 7e2745a137..0000000000 --- a/vector/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/vector/src/main/res/drawable/bottomsheet_handle.xml b/vector/src/main/res/drawable/bottomsheet_handle.xml new file mode 100644 index 0000000000..89ccf57ed0 --- /dev/null +++ b/vector/src/main/res/drawable/bottomsheet_handle.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_composer_collapse.xml b/vector/src/main/res/drawable/ic_composer_collapse.xml new file mode 100644 index 0000000000..724a833761 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_collapse.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_composer_full_screen.xml b/vector/src/main/res/drawable/ic_composer_full_screen.xml index 394dc52279..de1862c09b 100644 --- a/vector/src/main/res/drawable/ic_composer_full_screen.xml +++ b/vector/src/main/res/drawable/ic_composer_full_screen.xml @@ -1,9 +1,9 @@ - + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + diff --git a/vector/src/main/res/drawable/ic_composer_rich_mic_pressed.xml b/vector/src/main/res/drawable/ic_composer_rich_mic_pressed.xml new file mode 100644 index 0000000000..e9dbe610e4 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_rich_mic_pressed.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml b/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml new file mode 100644 index 0000000000..c461470de5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml b/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml new file mode 100644 index 0000000000..4556974221 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_save.xml b/vector/src/main/res/drawable/ic_composer_rich_text_save.xml new file mode 100644 index 0000000000..f270d6f8ae --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_rich_text_save.xml @@ -0,0 +1,16 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_rich_composer_add.xml b/vector/src/main/res/drawable/ic_rich_composer_add.xml new file mode 100644 index 0000000000..3a90a40902 --- /dev/null +++ b/vector/src/main/res/drawable/ic_rich_composer_add.xml @@ -0,0 +1,15 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_rich_composer_send.xml b/vector/src/main/res/drawable/ic_rich_composer_send.xml new file mode 100644 index 0000000000..0f99c1670e --- /dev/null +++ b/vector/src/main/res/drawable/ic_rich_composer_send.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_voice_mic_recording.xml b/vector/src/main/res/drawable/ic_voice_mic_recording.xml deleted file mode 100644 index a57852c92f..0000000000 --- a/vector/src/main/res/drawable/ic_voice_mic_recording.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index fb0d80278a..7c465891c3 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -1,148 +1,210 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - + android:visibility="gone" + tools:visibility="visible"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml deleted file mode 100644 index 81b978caa6..0000000000 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml deleted file mode 100644 index 8cdb388bf9..0000000000 --- a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index c5afe1eb44..5f37de2a3a 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -1,183 +1,201 @@ - - - - - - - - - - - - - - - - - - - - - - + android:orientation="vertical" + android:background="@drawable/bg_composer_rich_bottom_sheet"> - - - - - - - - - - - - - + + + + + + + + + + + + + + + android:layout_marginStart="6dp" + android:layout_marginTop="8dp" + android:paddingBottom="2dp" + android:fontFamily="sans-serif-medium" + tools:text="Editing" + style="@style/BottomSheetItemTime" + app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder" + app:layout_constraintStart_toEndOf="@id/composerModeIconView" /> + + + + + + + + + + + + + + + + + + - + - + - + - + diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml deleted file mode 100644 index 1a3023a805..0000000000 --- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml deleted file mode 100644 index b0380d2e13..0000000000 --- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml deleted file mode 100644 index 3105063933..0000000000 --- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/fragment_composer.xml b/vector/src/main/res/layout/fragment_composer.xml index 41c052367a..5038a9e179 100644 --- a/vector/src/main/res/layout/fragment_composer.xml +++ b/vector/src/main/res/layout/fragment_composer.xml @@ -4,12 +4,13 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="@android:color/transparent"> - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:id="@+id/rootConstraintLayout"> - + - - - - - - - - - + - + - - - - + app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" + tools:listitem="@layout/item_timeline_event_base" /> - + + + + + + + + + + + + + + + + app:layout_constraintTop_toTopOf="parent" + tools:visibility="gone"> + + + + + + - + + + + + diff --git a/vector/src/main/res/layout/fragment_timeline_fullscreen.xml b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml deleted file mode 100644 index 373ca74f56..0000000000 --- a/vector/src/main/res/layout/fragment_timeline_fullscreen.xml +++ /dev/null @@ -1,258 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/view_rich_text_menu_button.xml b/vector/src/main/res/layout/view_rich_text_menu_button.xml index 24b19c10b5..b99a29da2b 100644 --- a/vector/src/main/res/layout/view_rich_text_menu_button.xml +++ b/vector/src/main/res/layout/view_rich_text_menu_button.xml @@ -2,8 +2,8 @@ + + @@ -109,7 +118,7 @@ android:id="@+id/voiceMessageLockImage" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="28dp" + android:layout_marginTop="16dp" android:importantForAccessibility="no" android:src="@drawable/ic_voice_message_unlocked" android:visibility="gone" @@ -123,7 +132,6 @@ android:id="@+id/voiceMessageLockArrow" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="4dp" android:layout_marginBottom="14dp" android:importantForAccessibility="no" android:src="@drawable/ic_voice_lock_arrow" From 208bf6eb2e3378a3944ee54d80d8742904985c00 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 17 Nov 2022 14:32:37 +0100 Subject: [PATCH 265/679] First version of the release script. Release the APP. --- tools/release/releaseScript.sh | 239 +++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100755 tools/release/releaseScript.sh diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh new file mode 100755 index 0000000000..f7f5bf17c1 --- /dev/null +++ b/tools/release/releaseScript.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash + +# +# Copyright (c) 2022 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Ignore any error to not stop the script +set +e + +printf "\n" +printf "================================================================================\n" +printf "| Welcome to the release script! |\n" +printf "================================================================================\n" + +releaseScriptLocation="${RELEASE_SCRIPT_PATH}" + +if [[ -z "${releaseScriptLocation}" ]]; then + printf "Fatal: RELEASE_SCRIPT_PATH is not defined in the environment. Please set to the path of your local file 'releaseElement2.sh'.\n" + exit 1 +fi + +releaseScriptFullPath="${releaseScriptLocation}/releaseElement2.sh" + +if [[ ! -f ${releaseScriptFullPath} ]]; then + printf "Fatal: release script not found at ${releaseScriptFullPath}.\n" + exit 1 +fi + +# Guessing version to propose a default version +versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3` +versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut -d " " -f3` +versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut -d " " -f3` +versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}" + +printf "\n" +read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version +version=${version:-${versionCandidate}} + +# extract major, minor and patch for future use +versionMajor=`echo ${version} | cut -d "." -f1` +versionMinor=`echo ${version} | cut -d "." -f2` +versionPatch=`echo ${version} | cut -d "." -f3` +nextPatchVersion=$((versionPatch + 2)) + +printf "\n================================================================================\n" +printf "Ensuring main and develop branches are up to date...\n" + +git checkout main +git pull +git checkout develop +git pull + +printf "\n================================================================================\n" +printf "Starting the release ${version}\n" +git flow release start ${version} + +# Note: in case the release is already started and the script is started again, checkout the release branch again. +ret=$? +if [[ $ret -ne 0 ]]; then + printf "Mmh, it seems that the release is already started. Checking out the release branch...\n" + git checkout "release/${version}" +fi + +# Ensure version is OK +cp ./vector-app/build.gradle ./vector-app/build.gradle.bak +sed "s/ext.versionMajor = .*/ext.versionMajor = ${versionMajor}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle +sed "s/ext.versionMinor = .*/ext.versionMinor = ${versionMinor}/" ./vector-app/build.gradle > ./vector-app/build.gradle.bak +sed "s/ext.versionPatch = .*/ext.versionPatch = ${patchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle +rm ./vector-app/build.gradle.bak +cp ./matrix-sdk-android/build.gradle ./matrix-sdk-android/build.gradle.bak +sed "s/\"SDK_VERSION\", .*$/\"SDK_VERSION\", \"\\\\\"${version}\\\\\"\"/" ./matrix-sdk-android/build.gradle.bak > ./matrix-sdk-android/build.gradle +rm ./matrix-sdk-android/build.gradle.bak + +# This commit may have no effect because generally we do not change the version during the release. +commit -a -m "Setting version for the release ${version}" + +printf "\n================================================================================\n" +read -p "Please check the crashes from the PlayStore. You can commit fixes if any on the release branch. Press enter when it's done." + +printf "\n================================================================================\n" +read -p "Please check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/${version}-dev. You can commit fixes if any on the release branch. Press enter when it's done." + +printf "\n================================================================================\n" +read -p "Please make sure an emulator is running and press enter when it is ready." + +printf "\n================================================================================\n" +printf "Checking if Synapse is running...\n" +httpCode=`curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/_matrix/static` + +if [[ ${httpCode} -ne "302" ]]; then + read -p "Please make sure Synapse is running (open http://127.0.0.1:8080) and press enter when it is ready." +else + printf "Synapse is running!\n" +fi + +printf "\n================================================================================\n" +printf "Uninstalling previous test app if any...\n" +adb -e uninstall im.vector.app.debug.test + +printf "\n================================================================================\n" +printf "Running the integration test UiAllScreensSanityTest.allScreensTest()...\n" +./gradlew connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest + +printf "\n================================================================================\n" +printf "Building the app...\n" +./gradlew assembleGplayDebug + +printf "\n================================================================================\n" +printf "Uninstalling previous test app if any...\n" +adb -e uninstall im.vector.app.debug + +printf "\n================================================================================\n" +printf "Installing the app...\n" +adb -e install ./vector-app/build/outputs/apk/gplay/debug/vector-gplay-arm64-v8a-debug.apk + +printf "\n================================================================================\n" +printf "Running the app...\n" +# TODO This does not work, need to be fixed +adb -e shell am start -n im.vector.app.debug/im.vector.app.features.Alias -a android.intent.action.MAIN -c android.intent.category.LAUNCHER + +printf "\n================================================================================\n" +# TODO could build and deploy the APK to any emulator +read -p "Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Running towncrier...\n" +yes | towncrier build --version "v${version}" + +printf "\n================================================================================\n" +read -p "Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things. Do not commit your change. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Committing...\n" +git commit -a -m "Changelog for version ${version}" + +printf "\n================================================================================\n" +printf "Creating fastlane file...\n" +printf -v versionMajor2Digits "%02d" ${versionMajor} +printf -v versionMinor2Digits "%02d" ${versionMinor} +printf -v versionPatch2Digits "%02d" ${versionPatch} +fastlaneFile="4${versionMajor2Digits}${versionMinor2Digits}${versionPatch2Digits}0.txt" +fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}" +printf "Main changes in this version: TODO.\nFull changelog: https://github.com/vector-im/element-android/releases" > ${fastlanePathFile} + +read -p "I have created the file ${fastlanePathFile}, please edit it and press enter when it's done." +git commit -a -m "Adding fastlane file for version ${version}" + +printf "\n================================================================================\n" +# We could propose to push the branch and create a PR +read -p "(optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. Press enter when it's done." + +printf "\n================================================================================\n" +printf "OK, finishing the release...\n" +git flow release finish "${version}" + +printf "\n================================================================================\n" +read -p "Done, push the branch 'main' and the new tag (yes/no) default to yes? " doPush +doPush=${doPush:-yes} + +if [ ${doPush} == "yes" ]; then + printf "Pushing branch 'main' and tag 'v${version}'...\n" + git push origin main + git push origin "v${version}" +else + printf "Not pushing, do not forget to push manually!\n" +fi + +printf "\n================================================================================\n" +printf "Checking out develop...\n" +git checkout develop + +# Set next version +printf "\n================================================================================\n" +printf "Setting next version on file './vector-app/build.gradle'...\n" +nextPatchVersion=$((versionPatch + 2)) +cp ./vector-app/build.gradle ./vector-app/build.gradle.bak +sed "s/ext.versionPatch = .*/ext.versionPatch = ${nextPatchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle +rm ./vector-app/build.gradle.bak + +printf "\n================================================================================\n" +printf "Setting next version on file './matrix-sdk-android/build.gradle'...\n" +nextVersion="${versionMajor}.${versionMinor}.${nextPatchVersion}" +cp ./matrix-sdk-android/build.gradle ./matrix-sdk-android/build.gradle.bak +sed "s/\"SDK_VERSION\", .*$/\"SDK_VERSION\", \"\\\\\"${nextVersion}\\\\\"\"/" ./matrix-sdk-android/build.gradle.bak > ./matrix-sdk-android/build.gradle +rm ./matrix-sdk-android/build.gradle.bak + +printf "\n================================================================================\n" +read -p "I have updated the versions to prepare the next release, please check that the change are correct and press enter so I can commit." + +printf "Committing...\n" +git commit -a -m 'version++' + +printf "\n================================================================================\n" +read -p "Done, push the branch 'develop' (yes/no) default to yes? (A rebase may be necessary in case develop got new commits)" doPush +doPush=${doPush:-yes} + +if [ ${doPush} == "yes" ]; then + printf "Pushing branch 'develop'...\n" + git push origin develop +else + printf "Not pushing, do not forget to push manually!\n" +fi + +printf "\n================================================================================\n" +read -p "Wait for Buildkite https://buildkite.com/matrix-dot-org/element-android/builds?branch=main to build the 'main' branch. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Running the release script...\n" +cd ${releaseScriptLocation} +${releaseScriptFullPath} "v${version}" +cd - + +printf "\n================================================================================\n" +apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk" +printf "Installing apk on a real device...\n" +adb -d install ${apkPath} + +read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." +# TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)? +read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done." + +read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done." +read -p "Ping the Android Internal room. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Congratulation! Kudos for using this script! Have a nice day!\n" +printf "================================================================================\n" From 7d8bbd6d66a041b8fa9cd7162de610d8e9b9fdad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Nov 2022 10:26:56 +0100 Subject: [PATCH 266/679] Update the issue template --- .github/ISSUE_TEMPLATE/release.yml | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index a84f4dfd3b..ff407ed8c1 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -24,27 +24,7 @@ body: ### Do the release - - [ ] Make sure `develop` and `main` are up to date and create a release with gitflow: `git checkout main; git pull; git checkout develop; git pull; git flow release start '1.2.3'` - - [ ] Check the crashes from the PlayStore - - [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev - - [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()` - - [ ] Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance - - [ ] Run towncrier: `towncrier build --version v1.2.3 --draft` (remove `--draft` do write the file CHANGES.md) - - [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things - - [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs - - [ ] (optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes. - - [ ] Finish release with gitflow, delete the draft PR (if created): `git flow release finish '1.2.3'` - - [ ] Push `main` and the new tag `v1.2.3` to origin: `git push origin main; git push origin 'v1.2.3'` - - [ ] Checkout `develop`: `git checkout develop` - - [ ] Increase version (versionPatch + 2) in `./vector/build.gradle` - - [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle` - - [ ] Commit and push `develop`: `git commit -m 'version++'; git push origin develop` - - [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch. - - [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them. - - [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.) - - [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md - - [ ] Add the 4 signed APKs to the GitHub release - - [ ] Ping the Android Internal room + - [ ] Run the script ./tools/release/releaseScript.sh and follow the steps. ### Once tested and validated internally From 729d420c2c153ac01225e618431336af19e45c33 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 18 Nov 2022 10:30:59 +0100 Subject: [PATCH 267/679] Propose message for internal room --- tools/release/releaseScript.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index f7f5bf17c1..dcb8fec40a 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -232,7 +232,11 @@ read -p "Please run the APK on your phone to check that the upgrade went well (n read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done." read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done." -read -p "Ping the Android Internal room. Press enter when it's done." + +printf "\n================================================================================\n" +printf "Ping the Android Internal room. Here is an example of message which can be sent:\n\n" +printf "@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!\n\n" +read -p "Press enter when it's done." printf "\n================================================================================\n" printf "Congratulation! Kudos for using this script! Have a nice day!\n" From 7774f6931760badd4d4d47b6f701e7d177529bd1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 18 Nov 2022 14:46:03 +0300 Subject: [PATCH 268/679] Fix unit test. --- .../src/androidTest/java/im/vector/app/core/utils/TestSpan.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt index 9e23e76f0c..e31dc6942c 100644 --- a/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt +++ b/vector/src/androidTest/java/im/vector/app/core/utils/TestSpan.kt @@ -29,6 +29,7 @@ import io.mockk.verify import io.noties.markwon.core.spans.EmphasisSpan import io.noties.markwon.core.spans.OrderedListItemSpan import io.noties.markwon.core.spans.StrongEmphasisSpan +import me.gujun.android.span.style.CustomTypefaceSpan fun Spannable.toTestSpan(): String { var output = toString() @@ -54,7 +55,7 @@ private fun Any.readTags(): SpanTags { OrderedListItemSpan::class -> SpanTags("[list item]", "[/list item]") HtmlCodeSpan::class -> SpanTags("[code]", "[/code]") StrongEmphasisSpan::class -> SpanTags("[bold]", "[/bold]") - EmphasisSpan::class -> SpanTags("[italic]", "[/italic]") + EmphasisSpan::class, CustomTypefaceSpan::class -> SpanTags("[italic]", "[/italic]") else -> throw IllegalArgumentException("Unknown ${this::class}") } } From 6c45490dd19034b407d07fcab60b5aff23074bf1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 21 Nov 2022 18:44:45 +0300 Subject: [PATCH 269/679] Code review fixes. --- .../src/main/res/values/config-settings.xml | 1 + .../features/settings/VectorPreferences.kt | 10 ++++++ .../settings/devices/v2/DevicesViewModel.kt | 12 ++----- .../othersessions/OtherSessionsViewModel.kt | 12 ++----- .../v2/overview/SessionOverviewViewModel.kt | 12 ++----- .../main/res/layout/item_other_session.xml | 6 ++-- .../devices/v2/DevicesViewModelTest.kt | 33 ++++++++++++++++--- .../OtherSessionsViewModelTest.kt | 8 ++--- .../overview/SessionOverviewViewModelTest.kt | 8 ++--- .../app/test/fakes/FakeSharedPreferences.kt | 5 --- .../app/test/fakes/FakeVectorPreferences.kt | 4 +++ 11 files changed, 65 insertions(+), 46 deletions(-) diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index 504c587b8d..ef9695a080 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -51,6 +51,7 @@ false true false + false diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 3424c2b54c..30b5ef9131 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -1231,4 +1231,14 @@ class VectorPreferences @Inject constructor( return vectorFeatures.isVoiceBroadcastEnabled() && defaultPrefs.getBoolean(SETTINGS_LABS_VOICE_BROADCAST_KEY, getDefault(R.bool.settings_labs_enable_voice_broadcast_default)) } + + fun showIpAddressInDeviceManagerScreens(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, getDefault(R.bool.settings_device_manager_show_ip_address)) + } + + fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { + defaultPrefs.edit { + putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index b0ca32016f..971fb123f0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -16,15 +16,12 @@ package im.vector.app.features.settings.devices.v2 -import android.content.SharedPreferences -import androidx.core.content.edit import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler @@ -53,8 +50,7 @@ class DevicesViewModel @AssistedInject constructor( private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, - @DefaultPreferences - private val sharedPreferences: SharedPreferences, + private val vectorPreferences: VectorPreferences, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @AssistedFactory @@ -73,7 +69,7 @@ class DevicesViewModel @AssistedInject constructor( } private fun refreshIpAddressVisibility() { - val shouldShowIpAddress = sharedPreferences.getBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, false) + val shouldShowIpAddress = vectorPreferences.showIpAddressInDeviceManagerScreens() setState { copy(isShowingIpAddress = shouldShowIpAddress) } @@ -135,9 +131,7 @@ class DevicesViewModel @AssistedInject constructor( setState { copy(isShowingIpAddress = !isShowingIpAddress) } - sharedPreferences.edit { - putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, !isShowingIpAddress) - } + vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!isShowingIpAddress) } private fun handleVerifyCurrentSessionAction() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index fb20f0fd31..0f1dcca4cc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -16,15 +16,12 @@ package im.vector.app.features.settings.devices.v2.othersessions -import android.content.SharedPreferences -import androidx.core.content.edit import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler @@ -47,8 +44,7 @@ class OtherSessionsViewModel @AssistedInject constructor( private val signoutSessionsUseCase: SignoutSessionsUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, - @DefaultPreferences - private val sharedPreferences: SharedPreferences, + private val vectorPreferences: VectorPreferences, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase ) { @@ -68,7 +64,7 @@ class OtherSessionsViewModel @AssistedInject constructor( } private fun refreshIpAddressVisibility() { - val shouldShowIpAddress = sharedPreferences.getBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, false) + val shouldShowIpAddress = vectorPreferences.showIpAddressInDeviceManagerScreens() setState { copy(isShowingIpAddress = shouldShowIpAddress) } @@ -108,9 +104,7 @@ class OtherSessionsViewModel @AssistedInject constructor( setState { copy(isShowingIpAddress = !isShowingIpAddress) } - sharedPreferences.edit { - putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, !isShowingIpAddress) - } + vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!isShowingIpAddress) } private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index a105c40e9c..6c159fd50e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -16,15 +16,12 @@ package im.vector.app.features.settings.devices.v2.overview -import android.content.SharedPreferences -import androidx.core.content.edit import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler @@ -58,8 +55,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private val togglePushNotificationUseCase: TogglePushNotificationUseCase, private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, - @DefaultPreferences - private val sharedPreferences: SharedPreferences, + private val vectorPreferences: VectorPreferences, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase ) { @@ -80,7 +76,7 @@ class SessionOverviewViewModel @AssistedInject constructor( } private fun refreshIpAddressVisibility() { - val shouldShowIpAddress = sharedPreferences.getBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, false) + val shouldShowIpAddress = vectorPreferences.showIpAddressInDeviceManagerScreens() setState { copy(isShowingIpAddress = shouldShowIpAddress) } @@ -134,9 +130,7 @@ class SessionOverviewViewModel @AssistedInject constructor( setState { copy(isShowingIpAddress = !isShowingIpAddress) } - sharedPreferences.edit { - putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, !isShowingIpAddress) - } + vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!isShowingIpAddress) } private fun handleVerifySessionAction() = withState { viewState -> diff --git a/vector/src/main/res/layout/item_other_session.xml b/vector/src/main/res/layout/item_other_session.xml index dee96d2b2f..a6205e7d50 100644 --- a/vector/src/main/res/layout/item_other_session.xml +++ b/vector/src/main/res/layout/item_other_session.xml @@ -13,7 +13,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/bg_other_session" - app:layout_constraintBottom_toBottomOf="@id/otherSessionVerificationStatusImageView" + app:layout_constraintBottom_toBottomOf="@id/otherSessionSeparator" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -53,11 +53,12 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="8dp" + android:layout_marginTop="8dp" android:ellipsize="end" android:lines="1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/otherSessionDeviceTypeImageView" - app:layout_constraintTop_toTopOf="@id/otherSessionDeviceTypeImageView" + app:layout_constraintTop_toTopOf="@id/otherSessionItemBackground" tools:text="Element Mobile: Android" /> () private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) - private val fakeSharedPreferences = FakeSharedPreferences() + private val fakeVectorPreferences = FakeVectorPreferences() private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -87,7 +89,7 @@ class DevicesViewModelTest { interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, - sharedPreferences = fakeSharedPreferences, + vectorPreferences = fakeVectorPreferences.instance, ) } @@ -100,7 +102,7 @@ class DevicesViewModelTest { givenVerificationService() givenCurrentSessionCrossSigningInfo() givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) - fakeSharedPreferences.givenSessionManagerShowIpAddress(false) + fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { @@ -347,6 +349,29 @@ class DevicesViewModelTest { } } + @Test + fun `given the viewModel when initializing it then view state of ip address visibility is false`() { + // When + val viewModelTest = createViewModel().test() + + // Then + viewModelTest.assertLatestState { it.isShowingIpAddress == false } + viewModelTest.finish() + } + + @Test + fun `given the viewModel when toggleIpAddressVisibility action is triggered then view state and preference change accordingly`() { + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.ToggleIpAddressVisibility) + + // Then + viewModelTest.assertLatestState { it.isShowingIpAddress == true } + every { fakeVectorPreferences.instance.setIpAddressVisibilityInDeviceManagerScreens(true) } just runs + viewModelTest.finish() + } + private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo { val currentSessionCrossSigningInfo = mockk() every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index d6ed5a61a7..054369ec9f 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -25,8 +25,8 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler -import im.vector.app.test.fakes.FakeSharedPreferences import im.vector.app.test.fakes.FakeSignoutSessionsUseCase +import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -67,7 +67,7 @@ class OtherSessionsViewModelTest { private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakePendingAuthHandler = FakePendingAuthHandler() - private val fakeSharedPreferences = FakeSharedPreferences() + private val fakeVectorPreferences = FakeVectorPreferences() private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( @@ -77,7 +77,7 @@ class OtherSessionsViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, - sharedPreferences = fakeSharedPreferences, + vectorPreferences = fakeVectorPreferences.instance, ) @Before @@ -87,7 +87,7 @@ class OtherSessionsViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() - fakeSharedPreferences.givenSessionManagerShowIpAddress(false) + fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index c4ab82b7e8..3f81abd483 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -28,9 +28,9 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase import im.vector.app.test.fakes.FakePendingAuthHandler -import im.vector.app.test.fakes.FakeSharedPreferences import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test import im.vector.app.test.testDispatcher @@ -78,7 +78,7 @@ class SessionOverviewViewModelTest { private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() private val notificationsStatus = NotificationsStatus.ENABLED - private val fakeSharedPreferences = FakeSharedPreferences() + private val fakeVectorPreferences = FakeVectorPreferences() private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), @@ -91,7 +91,7 @@ class SessionOverviewViewModelTest { refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, - sharedPreferences = fakeSharedPreferences, + vectorPreferences = fakeVectorPreferences.instance, ) @Before @@ -106,7 +106,7 @@ class SessionOverviewViewModelTest { A_SESSION_ID_1, notificationsStatus ) - fakeSharedPreferences.givenSessionManagerShowIpAddress(false) + fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt index 0242bfe148..f9d525fd13 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedPreferences.kt @@ -18,7 +18,6 @@ package im.vector.app.test.fakes import android.content.SharedPreferences import im.vector.app.features.settings.FontScaleValue -import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS import io.mockk.every import io.mockk.mockk @@ -33,8 +32,4 @@ class FakeSharedPreferences : SharedPreferences by mockk() { every { contains("APPLICATION_USE_SYSTEM_FONT_SCALE_KEY") } returns true every { getBoolean("APPLICATION_USE_SYSTEM_FONT_SCALE_KEY", any()) } returns useSystemScale } - - fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { - every { getBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, any()) } returns showIpAddress - } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 4baa7e2b90..f05f5f1493 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -52,4 +52,8 @@ class FakeVectorPreferences { fun verifySetNotificationEnabledForDevice(enabled: Boolean, inverse: Boolean = false) { verify(inverse = inverse) { instance.setNotificationEnabledForDevice(enabled) } } + + fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { + every { instance.showIpAddressInDeviceManagerScreens() } returns showIpAddress + } } From 544c55444429d59bdffac2888e8e0b3987c35c2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Nov 2022 23:11:30 +0000 Subject: [PATCH 270/679] Bump io.gitlab.arturbosch.detekt from 1.21.0 to 1.22.0 Bumps io.gitlab.arturbosch.detekt from 1.21.0 to 1.22.0. --- updated-dependencies: - dependency-name: io.gitlab.arturbosch.detekt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5648acfba0..831abe0882 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ plugins { // ktlint Plugin id "org.jlleitschuh.gradle.ktlint" version "11.0.0" // Detekt - id "io.gitlab.arturbosch.detekt" version "1.21.0" + id "io.gitlab.arturbosch.detekt" version "1.22.0" // Ksp id "com.google.devtools.ksp" version "1.7.21-1.0.8" From b3965cae9dc4677e0e9725c7c4f6cd5f18f75d6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Nov 2022 07:53:58 +0100 Subject: [PATCH 271/679] Bump com.autonomousapps.dependency-analysis from 1.13.1 to 1.16.0 (#7622) Bumps com.autonomousapps.dependency-analysis from 1.13.1 to 1.16.0. --- updated-dependencies: - dependency-name: com.autonomousapps.dependency-analysis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5648acfba0..1380b6c2e9 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ plugins { id "com.google.devtools.ksp" version "1.7.21-1.0.8" // Dependency Analysis - id 'com.autonomousapps.dependency-analysis' version "1.13.1" + id 'com.autonomousapps.dependency-analysis' version "1.16.0" // Gradle doctor id "com.osacky.doctor" version "0.8.1" } From ab749eee6a174edc20dc371f8bc7479bfa02fc4f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 22 Nov 2022 16:05:37 +0300 Subject: [PATCH 272/679] Code review fixes. --- .../settings/devices/v2/DevicesViewModel.kt | 29 +++++++++++++----- .../v2/ToggleIpAddressVisibilityUseCase.kt | 30 +++++++++++++++++++ .../othersessions/OtherSessionsViewModel.kt | 27 ++++++++++++----- .../v2/overview/SessionOverviewViewModel.kt | 26 +++++++++++----- .../devices/v2/DevicesViewModelTest.kt | 9 +++++- .../OtherSessionsViewModelTest.kt | 3 ++ .../overview/SessionOverviewViewModelTest.kt | 3 ++ 7 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 971fb123f0..236237ddbb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2 +import android.content.SharedPreferences import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted @@ -51,7 +52,11 @@ class DevicesViewModel @AssistedInject constructor( private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, -) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { + private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, +) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase), + SharedPreferences.OnSharedPreferenceChangeListener { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -66,6 +71,20 @@ class DevicesViewModel @AssistedInject constructor( refreshDevicesOnCryptoDevicesChange() refreshDeviceList() refreshIpAddressVisibility() + observePreferences() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + refreshIpAddressVisibility() + } + + private fun observePreferences() { + vectorPreferences.subscribeToChanges(this) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(this) + super.onCleared() } private fun refreshIpAddressVisibility() { @@ -126,12 +145,8 @@ class DevicesViewModel @AssistedInject constructor( } } - private fun handleToggleIpAddressVisibility() = withState { state -> - val isShowingIpAddress = state.isShowingIpAddress - setState { - copy(isShowingIpAddress = !isShowingIpAddress) - } - vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!isShowingIpAddress) + private fun handleToggleIpAddressVisibility() { + toggleIpAddressVisibilityUseCase.execute() } private fun handleVerifyCurrentSessionAction() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt new file mode 100644 index 0000000000..ef99d3489a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class ToggleIpAddressVisibilityUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute() { + val currentVisibility = vectorPreferences.showIpAddressInDeviceManagerScreens() + vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!currentVisibility) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index 0f1dcca4cc..bd6e31dd4d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.othersessions +import android.content.SharedPreferences import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted @@ -28,6 +29,7 @@ import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded @@ -45,9 +47,10 @@ class OtherSessionsViewModel @AssistedInject constructor( private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, + private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase -) { +), SharedPreferences.OnSharedPreferenceChangeListener { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -61,6 +64,20 @@ class OtherSessionsViewModel @AssistedInject constructor( init { observeDevices(initialState.currentFilter) refreshIpAddressVisibility() + observePreferences() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + refreshIpAddressVisibility() + } + + private fun observePreferences() { + vectorPreferences.subscribeToChanges(this) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(this) + super.onCleared() } private fun refreshIpAddressVisibility() { @@ -99,12 +116,8 @@ class OtherSessionsViewModel @AssistedInject constructor( } } - private fun handleToggleIpAddressVisibility() = withState { state -> - val isShowingIpAddress = state.isShowingIpAddress - setState { - copy(isShowingIpAddress = !isShowingIpAddress) - } - vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!isShowingIpAddress) + private fun handleToggleIpAddressVisibility() { + toggleIpAddressVisibilityUseCase.execute() } private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 6c159fd50e..d423d4a743 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.overview +import android.content.SharedPreferences import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted @@ -27,6 +28,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase @@ -56,9 +58,10 @@ class SessionOverviewViewModel @AssistedInject constructor( private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, + private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase -) { +), SharedPreferences.OnSharedPreferenceChangeListener { companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() @@ -73,8 +76,21 @@ class SessionOverviewViewModel @AssistedInject constructor( observeCurrentSessionInfo() observeNotificationsStatus(initialState.deviceId) refreshIpAddressVisibility() + observePreferences() } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + refreshIpAddressVisibility() + } + + private fun observePreferences() { + vectorPreferences.subscribeToChanges(this) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(this) + super.onCleared() + } private fun refreshIpAddressVisibility() { val shouldShowIpAddress = vectorPreferences.showIpAddressInDeviceManagerScreens() setState { @@ -125,12 +141,8 @@ class SessionOverviewViewModel @AssistedInject constructor( } } - private fun handleToggleIpAddressVisibility() = withState { state -> - val isShowingIpAddress = state.isShowingIpAddress - setState { - copy(isShowingIpAddress = !isShowingIpAddress) - } - vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!isShowingIpAddress) + private fun handleToggleIpAddressVisibility() { + toggleIpAddressVisibilityUseCase.execute() } private fun handleVerifySessionAction() = withState { viewState -> diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index e34dd8e180..30320806e0 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2 +import android.content.SharedPreferences import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule @@ -76,6 +77,7 @@ class DevicesViewModelTest { private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) private val fakeVectorPreferences = FakeVectorPreferences() + private val toggleIpAddressVisibilityUseCase = mockk() private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -90,6 +92,7 @@ class DevicesViewModelTest { pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, vectorPreferences = fakeVectorPreferences.instance, + toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, ) } @@ -364,11 +367,15 @@ class DevicesViewModelTest { // When val viewModel = createViewModel() val viewModelTest = viewModel.test() + every { toggleIpAddressVisibilityUseCase.execute() } just runs + every { fakeVectorPreferences.instance.setIpAddressVisibilityInDeviceManagerScreens(true) } just runs + every { fakeVectorPreferences.instance.showIpAddressInDeviceManagerScreens() } returns true + viewModel.handle(DevicesAction.ToggleIpAddressVisibility) + viewModel.onSharedPreferenceChanged(null, null) // Then viewModelTest.assertLatestState { it.isShowingIpAddress == true } - every { fakeVectorPreferences.instance.setIpAddressVisibilityInDeviceManagerScreens(true) } just runs viewModelTest.finish() } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 054369ec9f..82f40d911d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler @@ -68,6 +69,7 @@ class OtherSessionsViewModelTest { private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakeVectorPreferences = FakeVectorPreferences() + private val toggleIpAddressVisibilityUseCase = mockk() private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( @@ -78,6 +80,7 @@ class OtherSessionsViewModelTest { pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, vectorPreferences = fakeVectorPreferences.instance, + toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, ) @Before diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 3f81abd483..287bdd159c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase @@ -79,6 +80,7 @@ class SessionOverviewViewModelTest { private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() private val notificationsStatus = NotificationsStatus.ENABLED private val fakeVectorPreferences = FakeVectorPreferences() + private val toggleIpAddressVisibilityUseCase = mockk() private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), @@ -92,6 +94,7 @@ class SessionOverviewViewModelTest { togglePushNotificationUseCase = togglePushNotificationUseCase.instance, getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, vectorPreferences = fakeVectorPreferences.instance, + toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, ) @Before From 5eb786b55f854ff7950115448655cd0a7f0303f5 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 22 Nov 2022 16:11:20 +0300 Subject: [PATCH 273/679] Code review fixes. --- .../app/features/settings/devices/v2/list/SessionInfoView.kt | 3 +-- .../app/features/settings/devices/v2/DevicesViewModelTest.kt | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 6e7e57fc49..c6044d04a4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -188,8 +188,7 @@ class SessionInfoView @JvmOverloads constructor( } else { views.sessionInfoLastActivityTextView.isGone = true } - views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible }) - views.sessionInfoLastIPAddressTextView.isVisible = isShowingIpAddress + views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress }) } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 30320806e0..16ccaab37f 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -16,7 +16,6 @@ package im.vector.app.features.settings.devices.v2 -import android.content.SharedPreferences import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule From abea9b686a3407eea3e343061ae7dd5ab96ca38a Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 22 Nov 2022 18:02:48 +0300 Subject: [PATCH 274/679] Code review fixes. --- vector-config/src/main/res/values/config-settings.xml | 3 ++- .../java/im/vector/app/features/settings/VectorPreferences.kt | 4 ++-- .../app/features/settings/devices/v2/DevicesViewModel.kt | 2 +- .../settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt | 2 +- .../devices/v2/othersessions/OtherSessionsViewModel.kt | 2 +- .../settings/devices/v2/overview/SessionOverviewViewModel.kt | 2 +- .../app/features/settings/devices/v2/DevicesViewModelTest.kt | 2 +- .../java/im/vector/app/test/fakes/FakeVectorPreferences.kt | 2 +- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index ef9695a080..ad9c16c214 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -51,12 +51,13 @@ false true false - false + + false diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 30b5ef9131..447038d768 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -1232,8 +1232,8 @@ class VectorPreferences @Inject constructor( defaultPrefs.getBoolean(SETTINGS_LABS_VOICE_BROADCAST_KEY, getDefault(R.bool.settings_labs_enable_voice_broadcast_default)) } - fun showIpAddressInDeviceManagerScreens(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, getDefault(R.bool.settings_device_manager_show_ip_address)) + fun showIpAddressInSessionManagerScreens(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, getDefault(R.bool.settings_session_manager_show_ip_address)) } fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 236237ddbb..f42d5af398 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -88,7 +88,7 @@ class DevicesViewModel @AssistedInject constructor( } private fun refreshIpAddressVisibility() { - val shouldShowIpAddress = vectorPreferences.showIpAddressInDeviceManagerScreens() + val shouldShowIpAddress = vectorPreferences.showIpAddressInSessionManagerScreens() setState { copy(isShowingIpAddress = shouldShowIpAddress) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt index ef99d3489a..1e1dc19c96 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt @@ -24,7 +24,7 @@ class ToggleIpAddressVisibilityUseCase @Inject constructor( ) { fun execute() { - val currentVisibility = vectorPreferences.showIpAddressInDeviceManagerScreens() + val currentVisibility = vectorPreferences.showIpAddressInSessionManagerScreens() vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!currentVisibility) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index bd6e31dd4d..a5282e7ba2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -81,7 +81,7 @@ class OtherSessionsViewModel @AssistedInject constructor( } private fun refreshIpAddressVisibility() { - val shouldShowIpAddress = vectorPreferences.showIpAddressInDeviceManagerScreens() + val shouldShowIpAddress = vectorPreferences.showIpAddressInSessionManagerScreens() setState { copy(isShowingIpAddress = shouldShowIpAddress) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index d423d4a743..472e0a4269 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -92,7 +92,7 @@ class SessionOverviewViewModel @AssistedInject constructor( super.onCleared() } private fun refreshIpAddressVisibility() { - val shouldShowIpAddress = vectorPreferences.showIpAddressInDeviceManagerScreens() + val shouldShowIpAddress = vectorPreferences.showIpAddressInSessionManagerScreens() setState { copy(isShowingIpAddress = shouldShowIpAddress) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 16ccaab37f..03177aac47 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -368,7 +368,7 @@ class DevicesViewModelTest { val viewModelTest = viewModel.test() every { toggleIpAddressVisibilityUseCase.execute() } just runs every { fakeVectorPreferences.instance.setIpAddressVisibilityInDeviceManagerScreens(true) } just runs - every { fakeVectorPreferences.instance.showIpAddressInDeviceManagerScreens() } returns true + every { fakeVectorPreferences.instance.showIpAddressInSessionManagerScreens() } returns true viewModel.handle(DevicesAction.ToggleIpAddressVisibility) viewModel.onSharedPreferenceChanged(null, null) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index f05f5f1493..d89764a77e 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -54,6 +54,6 @@ class FakeVectorPreferences { } fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { - every { instance.showIpAddressInDeviceManagerScreens() } returns showIpAddress + every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress } } From 1fe790e46fd7f2e377e6378b7d5c6eb1b5af93db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Nov 2022 19:12:07 +0100 Subject: [PATCH 275/679] Bump wysiwyg from 0.4.0 to 0.7.0 (#7572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump wysiwyg from 0.4.0 to 0.6.0 Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.4.0 to 0.6.0. - [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases) - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/RELEASE.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/0.4.0...0.6.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update RTE library to 0.7.0 * Fix markdown -> html Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín --- dependencies.gradle | 2 +- .../detail/composer/RichTextComposerLayout.kt | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 8fc38cbbab..ac65035b60 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -98,7 +98,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.4.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.7.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 85f163360f..880ee2c031 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -48,8 +48,8 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding import io.element.android.wysiwyg.EditorEditText import io.element.android.wysiwyg.inputhandlers.models.InlineFormat +import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ComposerAction -import uniffi.wysiwyg_composer.MenuState class RichTextComposerLayout @JvmOverloads constructor( context: Context, @@ -206,16 +206,16 @@ class RichTextComposerLayout @JvmOverloads constructor( } private fun setupRichTextMenu() { - addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.Bold) { + addRichTextMenuItem(R.drawable.ic_composer_bold, R.string.rich_text_editor_format_bold, ComposerAction.BOLD) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold) } - addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.Italic) { + addRichTextMenuItem(R.drawable.ic_composer_italic, R.string.rich_text_editor_format_italic, ComposerAction.ITALIC) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic) } - addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.Underline) { + addRichTextMenuItem(R.drawable.ic_composer_underlined, R.string.rich_text_editor_format_underline, ComposerAction.UNDERLINE) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline) } - addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.StrikeThrough) { + addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) } } @@ -238,12 +238,9 @@ class RichTextComposerLayout @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - views.richTextComposerEditText.menuStateChangedListener = EditorEditText.OnMenuStateChangedListener { state -> - if (state is MenuState.Update) { - updateMenuStateFor(ComposerAction.Bold, state) - updateMenuStateFor(ComposerAction.Italic, state) - updateMenuStateFor(ComposerAction.Underline, state) - updateMenuStateFor(ComposerAction.StrikeThrough, state) + views.richTextComposerEditText.actionStatesChangedListener = EditorEditText.OnActionStatesChangedListener { state -> + for (action in state.keys) { + updateMenuStateFor(action, state) } } @@ -261,9 +258,9 @@ class RichTextComposerLayout @JvmOverloads constructor( */ private fun syncEditTexts() = if (isTextFormattingEnabled) { - views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText()) + views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown()) } else { - views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString()) + views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString()) } private fun addRichTextMenuItem(@DrawableRes iconId: Int, @StringRes description: Int, action: ComposerAction, onClick: () -> Unit) { @@ -279,10 +276,11 @@ class RichTextComposerLayout @JvmOverloads constructor( } } - private fun updateMenuStateFor(action: ComposerAction, menuState: MenuState.Update) { + private fun updateMenuStateFor(action: ComposerAction, menuState: Map) { val button = findViewWithTag(action) ?: return - button.isEnabled = !menuState.disabledActions.contains(action) - button.isSelected = menuState.reversedActions.contains(action) + val stateForAction = menuState[action] + button.isEnabled = stateForAction != ActionState.DISABLED + button.isSelected = stateForAction == ActionState.REVERSED } fun estimateCollapsedHeight(): Int { From ed8fd345ce33a324ecece55358c7213407e60ebc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 23 Nov 2022 13:14:18 +0100 Subject: [PATCH 276/679] Add missing git command. --- tools/release/releaseScript.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index dcb8fec40a..685093d65e 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -84,7 +84,7 @@ sed "s/\"SDK_VERSION\", .*$/\"SDK_VERSION\", \"\\\\\"${version}\\\\\"\"/" ./matr rm ./matrix-sdk-android/build.gradle.bak # This commit may have no effect because generally we do not change the version during the release. -commit -a -m "Setting version for the release ${version}" +git commit -a -m "Setting version for the release ${version}" printf "\n================================================================================\n" read -p "Please check the crashes from the PlayStore. You can commit fixes if any on the release branch. Press enter when it's done." From 183570dce975fc933df6b104018d4a52efe0dc24 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 23 Nov 2022 13:16:40 +0100 Subject: [PATCH 277/679] Update the recipe for the SDK --- .github/ISSUE_TEMPLATE/release.yml | 33 ++---------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index ff407ed8c1..0c3542997a 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -61,29 +61,9 @@ body: The SDK2 and the sample app are released only when Element has been pushed to production. - - [ ] Checkout the `main` branch on Element Android project + - [ ] On the [SDK2 project](https://github.com/matrix-org/matrix-android-sdk2), run the script ./tools/releaseScript.sh and follow the instructions. - #### On the SDK2 project - - https://github.com/matrix-org/matrix-android-sdk2 - - - [ ] Create a release with GitFlow - - [ ] Update the value of VERSION_NAME in the file gradle.properties - - [ ] Update the files `./build.gradle` and `./gradle/gradle-wrapper.properties` manually, to use the latest version for the dependency. You can get inspired by the same files on Element Android project. - - [ ] Run the script `./tools/import_from_element.sh` - - [ ] Check the diff in the file `./matrix-sdk-android/build.gradle` and restore what may have been erased (in particular the line `apply plugin: "com.vanniktech.maven.publish"` and the line about the version) - - [ ] Let the script finish to build the library - - [ ] Update the file `CHANGES.md` - - [ ] Finish the release using GitFlow - - [ ] Push the branch `main`, the new tag and the branch `develop` to origin - - ##### Release on MavenCentral - - - [ ] Checkout the branch `main` - - [ ] Run the command `./gradlew publish --no-daemon --no-parallel`. You'll need some non-public element to do so - - [ ] Run the command `./gradlew closeAndReleaseRepository`. If it is working well, you can jump directly to the final step of this section. - - If `./gradlew closeAndReleaseRepository` fails (for instance, several repositories are waiting to be handled), you have to close and release the repository manually. Do the following steps: + Note: if the step `./gradlew closeAndReleaseRepository` fails (for instance, several repositories are waiting to be handled), you have to close and release the repository manually. Do the following steps: - [ ] Connect to https://s01.oss.sonatype.org - [ ] Click on Staging Repositories and check the the files have been uploaded @@ -91,15 +71,6 @@ body: - [ ] Wait (check Activity tab until step "Repository closed" is displayed) - [ ] Click on release. The staging repository will disappear - Final step - - - [ ] Check that the release is available in https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/ (it can take a few minutes) - - ##### Release on GitHub - - - [ ] Create the release on GitHub from [the tag](https://github.com/matrix-org/matrix-android-sdk2/tags) - - [ ] Upload the AAR on the GitHub release - ### Android SDK2 sample https://github.com/matrix-org/matrix-android-sdk2-sample From 212074470f69b04397362cf8750299cb852172a6 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 16 Nov 2022 15:04:46 +0000 Subject: [PATCH 278/679] Translated using Weblate (Czech) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- library/ui-strings/src/main/res/values-cs/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index b4f9698197..5ee8a6fd32 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2908,4 +2908,5 @@ Odhlásit se z %1$d relací Odhlásit se + zbývá %1$s \ No newline at end of file From 778462dfc878e4783634ca6483833b023b41ea87 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 15 Nov 2022 20:11:39 +0000 Subject: [PATCH 279/679] Translated using Weblate (German) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- library/ui-strings/src/main/res/values-de/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index 186533c8f9..4218f19f9f 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2851,4 +2851,5 @@ Von %1$d Sitzungen abmelden Abmelden + %1$s übrig \ No newline at end of file From 9eb4eb6dcf01170465d60712642f5789c5ea7513 Mon Sep 17 00:00:00 2001 From: iaiz Date: Sat, 19 Nov 2022 00:39:09 +0000 Subject: [PATCH 280/679] Translated using Weblate (Spanish) Currently translated at 90.2% (2296 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/es/ --- library/ui-strings/src/main/res/values-es/strings.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-es/strings.xml b/library/ui-strings/src/main/res/values-es/strings.xml index f73c4952c6..3d10997233 100644 --- a/library/ui-strings/src/main/res/values-es/strings.xml +++ b/library/ui-strings/src/main/res/values-es/strings.xml @@ -2649,4 +2649,10 @@ Crear sala Iniciar conversación Todas las conversaciones - + Seleccionar todo + De acuerdo + + %1$d seleccionado + %1$d seleccionados + + \ No newline at end of file From e9bd9ed788df6cf1b5903a41ed71580a55d2a6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 15 Nov 2022 19:42:11 +0000 Subject: [PATCH 281/679] Translated using Weblate (Estonian) Currently translated at 99.6% (2535 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- library/ui-strings/src/main/res/values-et/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 4747f03fee..cf5f23a240 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2843,4 +2843,5 @@ Logi välja %1$d\'st sessioonist Logi välja + jäänud %1$s \ No newline at end of file From 3c5bc7e6f2b9ad7b44617fa1af7cafaf0eef3e48 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 15 Nov 2022 20:07:39 +0000 Subject: [PATCH 282/679] Translated using Weblate (Persian) Currently translated at 99.6% (2533 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/ --- library/ui-strings/src/main/res/values-fa/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index 313734290f..fb3651dfa2 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -2825,4 +2825,10 @@ ۳۰ ثانیه پیش‌روی ۳۰ ثانیه پس‌روی قالب‌بندی متن + خروج + + خروج از ۱ نشست + خروج از %1$d نشست + + %1$s مانده \ No newline at end of file From 32462b9056f03cb594e9d277c5a67c28b10831d5 Mon Sep 17 00:00:00 2001 From: Glandos Date: Wed, 16 Nov 2022 09:20:18 +0000 Subject: [PATCH 283/679] Translated using Weblate (French) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- library/ui-strings/src/main/res/values-fr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index a8b5eb28ae..ca462c592f 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2852,4 +2852,5 @@ Déconnecter %1$d sessions Déconnecter + %1$s restant \ No newline at end of file From 5d1f6e0bbe4d6667e066f34647ed32edfb21e8a7 Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 16 Nov 2022 17:26:45 +0000 Subject: [PATCH 284/679] Translated using Weblate (Indonesian) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/id/ --- library/ui-strings/src/main/res/values-in/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index 8f7670d08f..7828ec8c13 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2798,4 +2798,5 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Keluarkan %1$d sesi Keluarkan + %1$s tersisa \ No newline at end of file From b745ebe90a1977a8663707b7bda1e9e6c41a4058 Mon Sep 17 00:00:00 2001 From: random Date: Wed, 16 Nov 2022 10:47:26 +0000 Subject: [PATCH 285/679] Translated using Weblate (Italian) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/it/ --- library/ui-strings/src/main/res/values-it/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index 4257d52c3e..2322561d05 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -2843,4 +2843,5 @@ Impossibile iniziare una nuova trasmissione vocale Manda avanti di 30 secondi Manda indietro di 30 secondi + %1$s rimasti \ No newline at end of file From f43a16ced42885f5aeb76bd148a17f1bbd170911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Romanik?= Date: Sat, 19 Nov 2022 01:58:12 +0000 Subject: [PATCH 286/679] Translated using Weblate (Polish) Currently translated at 91.8% (2336 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pl/ --- library/ui-strings/src/main/res/values-pl/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml index ab9c367824..1c01c82189 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -2715,7 +2715,7 @@ Preferencje interfejsu Przeglądaj pokoje Utwórz pokój - Zacznij rozmawiać + Rozpocznij czat Wszystkie rozmowy Nie zweryfikowano · Ostatnia aktywność %1$s Zweryfikowano · Ostatnia aktywność %1$s @@ -2743,4 +2743,4 @@ %s \nwygląda nieco pusto. Brak przestrzeni. - + \ No newline at end of file From b76b51265f10ae6456822f93f2aa4d71e5681e1e Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Wed, 16 Nov 2022 22:44:14 +0000 Subject: [PATCH 287/679] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/ --- .../ui-strings/src/main/res/values-pt-rBR/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index d3061371fa..a916255d6f 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -2844,4 +2844,13 @@ Não dá pra começar um novo broadcast de voz Avançar rápido 30 segundos Retroceder 30 segundos + Sessões verificadas são onde quer que você está usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada. +\n +\nIsto significa que você tem todas as chaves necessitadas para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão. + + Fazer signout de %1$d sessão + Fazer signout de %1$d sessões + + Fazer signout + %1$s restando \ No newline at end of file From 8296665b36a45967bbb7d231dd5fc0cdd419a9b7 Mon Sep 17 00:00:00 2001 From: Platon Terekhov Date: Sun, 20 Nov 2022 15:17:24 +0000 Subject: [PATCH 288/679] Translated using Weblate (Russian) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- library/ui-strings/src/main/res/values-ru/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index fe71c0b596..8cc7bc54e0 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -2949,4 +2949,5 @@ ⚠ В этой комнате есть неподтверждённые устройства, они не смогут расшифровывать сообщения, отправленные вами. Дать разрешение Другие пользователи могут найти вас по %s + Осталось %1$s \ No newline at end of file From 1ef3e48ab6be423b433595cb3b148f05c906bf62 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 15 Nov 2022 23:49:40 +0000 Subject: [PATCH 289/679] Translated using Weblate (Slovak) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index 9c15c5ec0d..acd69a2024 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2908,4 +2908,5 @@ Odhlásiť sa z %1$d relácií Odhlásiť sa + Ostáva %1$s \ No newline at end of file From 6e11874027e6965548a33bb3214245267a6aeae7 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 15 Nov 2022 20:01:59 +0000 Subject: [PATCH 290/679] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- library/ui-strings/src/main/res/values-uk/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 8affc63685..27511255ee 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2964,4 +2964,5 @@ Вийти з %1$d сеансів Вийти + Залишилося %1$s \ No newline at end of file From 2f69740e9b093a7474d0d3f7dd4382996a92d787 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 16 Nov 2022 02:29:20 +0000 Subject: [PATCH 291/679] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2543 of 2543 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/ --- library/ui-strings/src/main/res/values-zh-rTW/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index 6976854649..30db508f83 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2796,4 +2796,5 @@ 登出 %1$d 個工作階段 登出 + 剩餘 %1$s \ No newline at end of file From 452cfd3327496f021527b2771940070335b76bda Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 23 Nov 2022 17:56:37 +0100 Subject: [PATCH 292/679] [RTE] Change layout based on plain text / rich text mode (#7621) --- changelog.d/7620.bugfix | 1 + .../detail/composer/RichTextComposerLayout.kt | 41 ++++++++++++++++++- .../res/layout/composer_rich_text_layout.xml | 1 + 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7620.bugfix diff --git a/changelog.d/7620.bugfix b/changelog.d/7620.bugfix new file mode 100644 index 0000000000..55c0e423ad --- /dev/null +++ b/changelog.d/7620.bugfix @@ -0,0 +1 @@ +Make the plain text mode layout of the RTE more compact. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index 880ee2c031..4664c4213c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -34,6 +34,7 @@ import android.widget.ImageButton import android.widget.LinearLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable import androidx.core.view.isGone import androidx.core.view.isInvisible @@ -44,6 +45,7 @@ import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.core.extensions.showKeyboard +import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding import io.element.android.wysiwyg.EditorEditText @@ -72,6 +74,11 @@ class RichTextComposerLayout @JvmOverloads constructor( field = value updateTextFieldBorder(isFullScreen) updateEditTextVisibility() + updateFullScreenButtonVisibility() + // If formatting is no longer enabled and it's in full screen, minimise the editor + if (!value && isFullScreen) { + callback?.onFullScreenModeChanged() + } } override val text: Editable? @@ -105,6 +112,8 @@ class RichTextComposerLayout @JvmOverloads constructor( } } + private val dimensionConverter = DimensionConverter(resources) + fun setFullScreen(isFullScreen: Boolean) { editText.updateLayoutParams { height = if (isFullScreen) 0 else ViewGroup.LayoutParams.WRAP_CONTENT @@ -191,8 +200,7 @@ class RichTextComposerLayout @JvmOverloads constructor( } views.composerFullScreenButton.apply { - // There's no point in having full screen in landscape since there's almost no vertical space - isInvisible = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + updateFullScreenButtonVisibility() setOnClickListener { callback?.onFullScreenModeChanged() } @@ -251,6 +259,35 @@ class RichTextComposerLayout @JvmOverloads constructor( views.richTextComposerEditText.isVisible = isTextFormattingEnabled views.richTextMenu.isVisible = isTextFormattingEnabled views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled + + // The layouts for formatted text mode and plain text mode are different, so we need to update the constraints + val dpToPx = { dp: Int -> dimensionConverter.dpToPx(dp) } + ConstraintSet().apply { + clone(views.composerLayoutContent) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.TOP) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.START) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.END) + if (isTextFormattingEnabled) { + connect(R.id.composerEditTextOuterBorder, ConstraintSet.TOP, R.id.composerLayoutContent, ConstraintSet.TOP, dpToPx(8)) + connect(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM, R.id.sendButton, ConstraintSet.TOP, 0) + connect(R.id.composerEditTextOuterBorder, ConstraintSet.START, R.id.composerLayoutContent, ConstraintSet.START, dpToPx(12)) + connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.composerLayoutContent, ConstraintSet.END, dpToPx(12)) + } else { + connect(R.id.composerEditTextOuterBorder, ConstraintSet.TOP, R.id.composerLayoutContent, ConstraintSet.TOP, dpToPx(10)) + connect(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM, R.id.composerLayoutContent, ConstraintSet.BOTTOM, dpToPx(10)) + connect(R.id.composerEditTextOuterBorder, ConstraintSet.START, R.id.attachmentButton, ConstraintSet.END, 0) + connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.sendButton, ConstraintSet.START, 0) + } + applyTo(views.composerLayoutContent) + } + } + + private fun updateFullScreenButtonVisibility() { + val isLargeScreenDevice = resources.configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE) + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + // There's no point in having full screen in landscape since there's almost no vertical space + views.composerFullScreenButton.isInvisible = !isTextFormattingEnabled || (isLandscape && !isLargeScreenDevice) } /** diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index 5f37de2a3a..88f96c528e 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -26,6 +26,7 @@ From ffb5edd2e4ef0ac22ab215bc02d7638b7a6d7fe6 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 24 Nov 2022 09:46:11 +0100 Subject: [PATCH 293/679] Set timeout for test CI jobs (#7598) * Set timeout for test CI jobs * Increase timeout to 1.5h: some Test jobs successfully finish > 1h --- .github/workflows/post-pr.yml | 5 +++-- .github/workflows/tests.yml | 37 ++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index aac4fffa4e..af854bf371 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -16,7 +16,7 @@ env: jobs: # More info on should-i-run: - # If this fails to run (the IF doesn't complete) then the needs will not be satisfied for any of the + # If this fails to run (the IF doesn't complete) then the needs will not be satisfied for any of the # other jobs below, so none will run. # except for the notification job at the bottom which will run all the time, unless should-i-run isn't # successful, or all the other jobs have succeeded @@ -27,11 +27,12 @@ jobs: if: github.event.pull_request.merged # Additionally require PR to have been completely merged. steps: - run: echo "Run those tests!" # no-op success - + ui-tests: name: UI Tests (Synapse) needs: should-i-run runs-on: buildjet-4vcpu-ubuntu-2204 + timeout-minutes: 90 # We might need to increase it if the time for tests grows strategy: fail-fast: false matrix: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bb16d8abe8..931ec2da45 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,6 +14,7 @@ jobs: tests: name: Runs all tests runs-on: buildjet-4vcpu-ubuntu-2204 + timeout-minutes: 90 # We might need to increase it if the time for tests grows strategy: matrix: api-level: [28] @@ -126,26 +127,26 @@ jobs: # Unneeded as part of the test suite above, kept around in case we want to re-enable them. # # # Build Android Tests -# build-android-tests: -# name: Build Android Tests -# runs-on: ubuntu-latest +# build-android-tests: +# name: Build Android Tests +# runs-on: ubuntu-latest # concurrency: # group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('build-android-tests-{0}', github.ref) }} # cancel-in-progress: true -# steps: -# - uses: actions/checkout@v3 -# - uses: actions/setup-java@v3 -# with: -# distribution: 'adopt' -# java-version: 11 -# - uses: actions/cache@v3 -# with: -# path: | -# ~/.gradle/caches -# ~/.gradle/wrapper -# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} -# restore-keys: | -# ${{ runner.os }}-gradle- -# - name: Build Android Tests +# steps: +# - uses: actions/checkout@v3 +# - uses: actions/setup-java@v3 +# with: +# distribution: 'adopt' +# java-version: 11 +# - uses: actions/cache@v3 +# with: +# path: | +# ~/.gradle/caches +# ~/.gradle/wrapper +# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} +# restore-keys: | +# ${{ runner.os }}-gradle- +# - name: Build Android Tests # run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES From 3dccad9931572d98124481458778da607b83cfaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Nov 2022 10:58:42 +0100 Subject: [PATCH 294/679] Detekt ComplexMethod has been renamed to CyclomaticComplexMethod --- tools/detekt/detekt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 96adb3d117..62a4fc408f 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -55,7 +55,7 @@ complexity: active: false LongParameterList: active: false - ComplexMethod: + CyclomaticComplexMethod: active: false NestedBlockDepth: active: false From ebbfca4ffddd275663b3ff191fe08c8ac56e7785 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Nov 2022 11:06:00 +0100 Subject: [PATCH 295/679] Detekt: Use require() instead of throwing an IllegalArgumentException. [UseRequire] --- .../crypto/crosssigning/CrossSigningOlm.kt | 4 +- .../internal/crypto/tasks/EncryptEventTask.kt | 5 +- .../DefaultVerificationService.kt | 78 +++++++++---------- .../app/core/di/VectorViewModelFactory.kt | 4 +- 4 files changed, 40 insertions(+), 51 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt index 3218b99948..0f29404d4f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt @@ -83,9 +83,7 @@ internal class CrossSigningOlm @Inject constructor( val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me ?.get("ed25519:$pubKey") - if (signaturesMadeByMyKey.isNullOrBlank()) { - throw IllegalArgumentException("Not signed with my key $type") - } + require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" } // Check that Alice USK signature of Bob MSK is valid olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable)) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index f93da74507..5d2797a6af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -47,9 +47,8 @@ internal class DefaultEncryptEventTask @Inject constructor( // don't want to wait for any query // if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event val localEvent = params.event - if (localEvent.eventId == null || localEvent.type == null) { - throw IllegalArgumentException() - } + require(localEvent.eventId != null) + require(localEvent.type != null) localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index 1a04ee0302..5b400aa63f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -1140,28 +1140,25 @@ internal class DefaultVerificationService @Inject constructor( override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? { val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId) // should check if already one (and cancel it) - if (method == VerificationMethod.SAS) { - val tx = DefaultOutgoingSASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - txID, - otherUserId, - otherDeviceId - ) - tx.transport = verificationTransportToDeviceFactory.createTransport(tx) - addTransaction(tx) + require(method == VerificationMethod.SAS) { "Unknown verification method" } + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingKeyRequestManager, + secretShareManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + txID, + otherUserId, + otherDeviceId + ) + tx.transport = verificationTransportToDeviceFactory.createTransport(tx) + addTransaction(tx) - tx.start() - return txID - } else { - throw IllegalArgumentException("Unknown verification method") - } + tx.start() + return txID } override fun requestKeyVerificationInDMs( @@ -1343,28 +1340,25 @@ internal class DefaultVerificationService @Inject constructor( otherUserId: String, otherDeviceId: String ): String { - if (method == VerificationMethod.SAS) { - val tx = DefaultOutgoingSASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - transactionId, - otherUserId, - otherDeviceId - ) - tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) - addTransaction(tx) + require(method == VerificationMethod.SAS) { "Unknown verification method" } + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingKeyRequestManager, + secretShareManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + transactionId, + otherUserId, + otherDeviceId + ) + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) + addTransaction(tx) - tx.start() - return transactionId - } else { - throw IllegalArgumentException("Unknown verification method") - } + tx.start() + return transactionId } override fun readyPendingVerificationInDMs( diff --git a/vector/src/main/java/im/vector/app/core/di/VectorViewModelFactory.kt b/vector/src/main/java/im/vector/app/core/di/VectorViewModelFactory.kt index ded2a562c5..a14b12e7a5 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorViewModelFactory.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorViewModelFactory.kt @@ -37,9 +37,7 @@ class VectorViewModelFactory @Inject constructor( } } } - if (creator == null) { - throw IllegalArgumentException("Unknown model class: $modelClass") - } + require(creator != null) { "Unknown model class: $modelClass" } try { @Suppress("UNCHECKED_CAST") return creator.get() as T From 891709ef41b2cf22548e2302e583d5cbb4ec2c42 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 26 Oct 2022 17:05:31 +0200 Subject: [PATCH 296/679] better replace handling --- .../events/model/AggregatedAnnotation.kt | 1 - .../events/model/AggregatedRelations.kt | 1 + .../session/events/model/AggregatedReplace.kt | 33 ++ .../events/model/ValidDecryptedEvent.kt | 53 +++ .../room/model/EditAggregatedSummary.kt | 4 +- .../session/room/timeline/TimelineEvent.kt | 17 +- .../sdk/internal/database/AsyncTransaction.kt | 6 +- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/helper/ChunkEntityHelper.kt | 1 - .../database/helper/ThreadSummaryHelper.kt | 27 -- .../mapper/EventAnnotationsSummaryMapper.kt | 20 +- .../database/migration/MigrateSessionTo008.kt | 6 +- .../database/migration/MigrateSessionTo043.kt | 43 ++ .../model/EditAggregatedSummaryEntity.kt | 5 +- .../model/EventAnnotationsSummaryEntity.kt | 16 - .../session/EventInsertLiveProcessor.kt | 2 +- .../session/call/CallEventProcessor.kt | 2 +- .../session/room/EventEditValidator.kt | 115 ++++++ .../EventRelationsAggregationProcessor.kt | 191 ++++----- .../room/create/CreateLocalRoomTask.kt | 10 +- .../room/create/RoomCreateEventProcessor.kt | 2 +- ...iveLocationShareRedactionEventProcessor.kt | 2 +- .../room/prune/RedactionEventProcessor.kt | 2 +- .../tombstone/RoomTombstoneEventProcessor.kt | 2 +- .../android/sdk/internal/util/Monarchy.kt | 2 +- .../session/room/EditValidationTest.kt | 366 ++++++++++++++++++ .../android/sdk/test/fakes/FakeMonarchy.kt | 2 +- .../sdk/test/fakes/FakeRealmConfiguration.kt | 2 +- .../app/core/extensions/TimelineEvent.kt | 4 +- .../timeline/TimelineEventController.kt | 1 + .../timeline/factory/MessageItemFactory.kt | 2 +- .../factory/TimelineItemFactoryParams.kt | 2 + .../helper/MessageInformationDataFactory.kt | 40 +- 33 files changed, 774 insertions(+), 212 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt index 239f749993..5b2ab77467 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt @@ -38,5 +38,4 @@ data class AggregatedAnnotation( override val limited: Boolean? = false, override val count: Int? = 0, val chunk: List? = null - ) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt index ae8ed3941f..1dedcce8b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -50,5 +50,6 @@ import com.squareup.moshi.JsonClass data class AggregatedRelations( @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null, + @Json(name = "m.replace") val replaces: AggregatedReplace? = null, @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt new file mode 100644 index 0000000000..2ae091a1a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Note that there can be multiple events with an m.replace relationship to a given event (for example, if an event is edited multiple times). + * These should be aggregated by the homeserver. + * https://spec.matrix.org/v1.4/client-server-api/#server-side-aggregation-of-mreplace-relationships + * + */ +@JsonClass(generateAdapter = true) +data class AggregatedReplace( + @Json(name = "event_id") val eventId: String? = null, + @Json(name = "origin_server_ts") val originServerTs: Long? = null, + @Json(name = "sender") val senderId: String? = null, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt new file mode 100644 index 0000000000..0cee077807 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.events.model + +data class ValidDecryptedEvent( + val type: String, + val eventId: String, + val clearContent: Content, + val prevContent: Content? = null, + val originServerTs: Long, + val cryptoSenderKey: String, + val roomId: String, + val unsignedData: UnsignedData? = null, + val redacts: String? = null, + val algorithm: String, +) + +fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { + if (!this.isEncrypted()) return null + val decryptedContent = this.getDecryptedContent() ?: return null + val eventId = this.eventId ?: return null + val roomId = this.roomId ?: return null + val type = this.getDecryptedType() ?: return null + val senderKey = this.getSenderKey() ?: return null + val algorithm = this.content?.get("algorithm") as? String ?: return null + + return ValidDecryptedEvent( + type = type, + eventId = eventId, + clearContent = decryptedContent, + prevContent = this.prevContent, + originServerTs = this.originServerTs ?: 0, + cryptoSenderKey = senderKey, + roomId = roomId, + unsignedData = this.unsignedData, + redacts = this.redacts, + algorithm = algorithm + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt index 67bab626cb..7d445a5cc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt @@ -15,10 +15,10 @@ */ package org.matrix.android.sdk.api.session.room.model -import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event data class EditAggregatedSummary( - val latestContent: Content? = null, + val latestEdit: Event? = null, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) val sourceEvents: List, val localEchos: List, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 6f4049de36..6ceced59d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -30,12 +30,8 @@ import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody -import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.sender.SenderInfo @@ -140,13 +136,12 @@ fun TimelineEvent.getEditedEventId(): String? { * Get last MessageContent, after a possible edition. */ fun TimelineEvent.getLastMessageContent(): MessageContent? { - return when (root.getClearType()) { - EventType.STICKER -> root.getClearContent().toModel() - in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - } + return ( + annotations?.editSummary?.latestEdit + ?.getClearContent()?.toModel()?.newContent + ?: root.getClearContent() + ) + .toModel() } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt index 7d263f1937..a1ea88a70c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -26,17 +26,17 @@ import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.system.measureTimeMillis -internal fun CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) { +internal fun CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: (realm: Realm) -> T) { asyncTransaction(monarchy.realmConfiguration, transaction) } -internal fun CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) { +internal fun CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: (realm: Realm) -> T) { launch { awaitTransaction(realmConfiguration, transaction) } } -internal suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T { +internal suspend fun awaitTransaction(config: RealmConfiguration, transaction: (realm: Realm) -> T): T { return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) { Realm.getInstance(config).use { bgRealm -> bgRealm.beginTransaction() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 30836c027e..388f962454 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -59,6 +59,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -67,7 +68,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 42L, + schemaVersion = 43L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -119,5 +120,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 40) MigrateSessionTo040(realm).perform() if (oldVersion < 41) MigrateSessionTo041(realm).perform() if (oldVersion < 42) MigrateSessionTo042(realm).perform() + if (oldVersion < 43) MigrateSessionTo043(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 221abe0df5..149a2eebfe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -83,7 +83,6 @@ internal fun ChunkEntity.addTimelineEvent( this.eventId = eventId this.roomId = roomId this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() - ?.also { it.cleanUp(eventEntity.sender) } this.readReceipts = readReceiptsSummaryEntity this.displayIndex = displayIndex this.ownedByThreadChunk = ownedByThreadChunk diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 193710f962..0ac8dc7902 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.helper import io.realm.Realm import io.realm.RealmQuery import io.realm.Sort -import io.realm.kotlin.createObject import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -103,32 +102,6 @@ internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent( } } -private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap): TimelineEventEntity { - val roomId = roomId - val eventId = eventId - val localId = TimelineEventEntity.nextId(realm) - val senderId = sender ?: "" - - val timelineEventEntity = realm.createObject().apply { - this.localId = localId - this.root = this@toTimelineEventEntity - this.eventId = eventId - this.roomId = roomId - this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() - ?.also { it.cleanUp(sender) } - this.ownedByThreadChunk = true // To skip it from the original event flow - val roomMemberContent = roomMemberContentsByUser[senderId] - this.senderAvatar = roomMemberContent?.avatarUrl - this.senderName = roomMemberContent?.displayName - isUniqueDisplayName = if (roomMemberContent?.displayName != null) { - computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser) - } else { - true - } - } - return timelineEventEntity -} - internal fun ThreadSummaryEntity.Companion.createOrUpdate( threadSummaryType: ThreadSummaryUpdateType, realm: Realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 6bbeb17fdd..5fb70ad1ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary +import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity internal object EventAnnotationsSummaryMapper { @@ -36,13 +37,22 @@ internal object EventAnnotationsSummaryMapper { ) }, editSummary = annotationsSummary.editSummary - ?.let { - val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null + ?.let { summary -> + /** + * The most recent event is determined by comparing origin_server_ts; + * if two or more replacement events have identical origin_server_ts, + * the event with the lexicographically largest event_id is treated as more recent. + */ + val latestEdition = summary.editions.sortedWith(compareBy { it.timestamp }.thenBy { it.eventId }) + .lastOrNull() ?: return@let null + // get the event and validate? + val editEvent = latestEdition.event + EditAggregatedSummary( - latestContent = ContentMapper.map(latestEdition.content), - sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } + latestEdit = editEvent?.asDomain(), + sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } .map { editionOfEvent -> editionOfEvent.eventId }, - localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } + localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } .map { editionOfEvent -> editionOfEvent.eventId }, lastEditTs = latestEdition.timestamp ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt index b61bf7e6fa..42a47a9a27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt @@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8 override fun doMigrate(realm: DynamicRealm) { val editionOfEventSchema = realm.schema.create("EditionOfEvent") - .addField(EditionOfEventFields.CONTENT, String::class.java) + .addField("content"/**EditionOfEventFields.CONTENT*/, String::class.java) .addField(EditionOfEventFields.EVENT_ID, String::class.java) .setRequired(EditionOfEventFields.EVENT_ID, true) - .addField(EditionOfEventFields.SENDER_ID, String::class.java) - .setRequired(EditionOfEventFields.SENDER_ID, true) + .addField("senderId" /*EditionOfEventFields.SENDER_ID*/, String::class.java) + .setRequired("senderId" /*EditionOfEventFields.SENDER_ID*/, true) .addField(EditionOfEventFields.TIMESTAMP, Long::class.java) .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt new file mode 100644 index 0000000000..a27d4fda3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.EditionOfEventFields +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) { + + override fun doMigrate(realm: DynamicRealm) { + // content(string) & senderId(string) have been removed and replaced by a link to the actual event + realm.schema.get("EditionOfEvent") + ?.removeField("senderId") + ?.removeField("content") + ?.addRealmObjectField(EditionOfEventFields.EVENT.`$`, realm.schema.get("EventEntity")!!) + ?.transform { dynamicObject -> + realm.where(EventEntity::javaClass.name) + .equalTo(EventEntityFields.EVENT_ID, dynamicObject.getString(EditionOfEventFields.EVENT_ID)) + .equalTo(EventEntityFields.SENDER, dynamicObject.getString("senderId")) + .findFirst() + .let { + dynamicObject.setObject(EditionOfEventFields.EVENT.`$`, it) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt index 61acd51dd4..7b7b90f82d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt @@ -32,9 +32,8 @@ internal open class EditAggregatedSummaryEntity( @RealmClass(embedded = true) internal open class EditionOfEvent( - var senderId: String = "", var eventId: String = "", - var content: String? = null, var timestamp: Long = 0, - var isLocalEcho: Boolean = false + var isLocalEcho: Boolean = false, + var event: EventEntity? = null, ) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt index 645998d0c0..9a201ab4e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -19,7 +19,6 @@ import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity -import timber.log.Timber internal open class EventAnnotationsSummaryEntity( @PrimaryKey @@ -32,21 +31,6 @@ internal open class EventAnnotationsSummaryEntity( var liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummaryEntity? = null, ) : RealmObject() { - /** - * Cleanup undesired editions, done by users different from the originalEventSender. - */ - fun cleanUp(originalEventSenderId: String?) { - originalEventSenderId ?: return - - editSummary?.editions?.filter { - it.senderId != originalEventSenderId - } - ?.forEach { - Timber.w("Deleting an edition from ${it.senderId} of event sent by $originalEventSenderId") - it.deleteFromRealm() - } - } - companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt index a650fa2d64..9741a7bd15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt @@ -24,7 +24,7 @@ internal interface EventInsertLiveProcessor { fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean - suspend fun process(realm: Realm, event: Event) + fun process(realm: Realm, event: Event) /** * Called after transaction. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index b15a647421..6a4abe9d34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -54,7 +54,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { eventsToPostProcess.add(event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt new file mode 100644 index 0000000000..dcf6ad54a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import javax.inject.Inject + +internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) { + + sealed class EditValidity { + object Valid : EditValidity() + data class Invalid(val reason: String) : EditValidity() + object Unknown : EditValidity() + } + + /** + *There are a number of requirements on replacement events, which must be satisfied for the replacement to be considered valid: + * As with all event relationships, the original event and replacement event must have the same room_id + * (i.e. you cannot send an event in one room and then an edited version in a different room). + * The original event and replacement event must have the same sender (i.e. you cannot edit someone else’s messages). + * The replacement and original events must have the same type (i.e. you cannot change the original event’s type). + * The replacement and original events must not have a state_key property (i.e. you cannot edit state events at all). + * The original event must not, itself, have a rel_type of m.replace (i.e. you cannot edit an edit — though you can send multiple edits for a single original event). + * The replacement event (once decrypted, if appropriate) must have an m.new_content property. + * + * If the original event was encrypted, the replacement should be too. + */ + fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity { + // we might not know the original event at that time. In this case we can't perform the validation + // Edits should be revalidated when the original event is received + if (originalEvent == null) { + return EditValidity.Unknown + } + + if (originalEvent.roomId != replaceEvent.roomId) { + return EditValidity.Invalid("original event and replacement event must have the same room_id") + } + if (originalEvent.isStateEvent() || replaceEvent.isStateEvent()) { + return EditValidity.Invalid("replacement and original events must not have a state_key property") + } + // check it's from same sender + + if (originalEvent.isEncrypted()) { + if (!replaceEvent.isEncrypted()) return EditValidity.Invalid("If the original event was encrypted, the replacement should be too") + val originalDecrypted = originalEvent.toValidDecryptedEvent() + ?: return EditValidity.Unknown // UTD can't decide + val replaceDecrypted = replaceEvent.toValidDecryptedEvent() + ?: return EditValidity.Unknown // UTD can't decide + + val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId + val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId + + if (originalDecrypted.clearContent.toModel()?.relatesTo?.type == RelationType.REPLACE) { + return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") + } + + if (originalCryptoSenderId == null || editCryptoSenderId == null) { + // mm what can we do? we don't know if it's cryptographically from same user? + // let valid and UI should display send by deleted device warning? + val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId + val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId + if (bestEffortOriginal != bestEffortEdit) { + return EditValidity.Invalid("original event and replacement event must have the same sender") + } + } else { + if (originalCryptoSenderId != editCryptoSenderId) { + return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender") + } + } + + if (originalDecrypted.type != replaceDecrypted.type) { + return EditValidity.Invalid("replacement and original events must have the same type") + } + if (replaceDecrypted.clearContent.toModel()?.newContent == null) { + return EditValidity.Invalid("replacement event must have an m.new_content property") + } + } else { + if (originalEvent.content.toModel()?.relatesTo?.type == RelationType.REPLACE) { + return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") + } + + // check the sender + if (originalEvent.senderId != replaceEvent.senderId) { + return EditValidity.Invalid("original event and replacement event must have the same sender") + } + if (originalEvent.type != replaceEvent.type) { + return EditValidity.Invalid("replacement and original events must have the same type") + } + if (replaceEvent.content.toModel()?.newContent == null) { + return EditValidity.Invalid("replacement event must have an m.new_content property") + } + } + + return EditValidity.Valid + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 24d4975eb9..ef1d8c1430 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -42,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity @@ -72,6 +72,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val sessionManager: SessionManager, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor, + private val editValidator: EventEditValidator, private val clock: Clock, ) : EventInsertLiveProcessor { @@ -79,13 +80,15 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.MESSAGE, EventType.REDACTION, EventType.REACTION, + // The aggregator handles verification events but just to render tiles in the timeline + // It's not participating in verfication it's self, just timeline display EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, // TODO Add ? - // EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA @@ -94,7 +97,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { try { // Temporary catch, should be removed val roomId = event.roomId if (roomId == null) { @@ -102,7 +105,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return } val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") - when (event.type) { + + // It might be a late decryption of the original event or a event received when back paginating? + // let's check if there is already a summary for it and do some cleaning + if (!isLocalEcho) { + EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId.orEmpty()) + .findFirst() + ?.editSummary + ?.editions + ?.forEach { editionOfEvent -> + EventEntity.where(realm, editionOfEvent.eventId).findFirst()?.asDomain()?.let { editEvent -> + when (editValidator.validateEdit(event, editEvent)) { + is EventEditValidator.EditValidity.Invalid -> { + // delete it, it was invalid + Timber.v("## Replace: Removing a previously accepted edit for event ${event.eventId}") + editionOfEvent.deleteFromRealm() + } + else -> { + // nop + } + } + } + } + } + + when (event.getClearType()) { EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") @@ -113,21 +140,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) - EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() - ?.let { - TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() - ?.forEach { tet -> tet.annotations = it } - } + // XXX do something for aggregated edits? + // it's a bit strange as it would require to do a server query to get the edition? } - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { + val relationContent = event.getRelationContent() + if (relationContent?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) + handleReplace(realm, event, roomId, isLocalEcho, relationContent.eventId) } } - EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_ACCEPT, @@ -142,73 +165,30 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } - + // As for now Live event processors are not receiving UTD events. + // They will get an update if the event is decrypted later EventType.ENCRYPTED -> { - // Relation type is in clear - val encryptedEventContent = event.content.toModel() - if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || - encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE - ) { - event.getClearContent().toModel()?.let { - if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - // A replace! - handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } else if (event.getClearType() in EventType.POLL_RESPONSE) { - sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - pollAggregationProcessor.handlePollResponseEvent(session, realm, event) - } - } - } - } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { - Timber.v("## SAS REF in room $roomId for event ${event.eventId}") - encryptedEventContent.relatesTo.eventId?.let { - handleVerification(realm, event, roomId, isLocalEcho, it) - } - } - in EventType.POLL_RESPONSE -> { - event.getClearContent().toModel(catchError = true)?.let { - sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - pollAggregationProcessor.handlePollResponseEvent(session, realm, event) - } - } - } - in EventType.POLL_END -> { - sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - getPowerLevelsHelper(event.roomId)?.let { - pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) - } - } - } - in EventType.BEACON_LOCATION_DATA -> { - handleBeaconLocationData(event, realm, roomId, isLocalEcho) - } - } - } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) { - // Reaction - if (event.getClearType() == EventType.REACTION) { - // we got a reaction!! - Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}") - handleReaction(realm, event, roomId, isLocalEcho) - } - } - // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations -// else if (event.unsignedData?.relations?.annotations != null) { -// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}") -// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) -// EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() -// ?.let { -// TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() -// ?.forEach { tet -> tet.annotations = it } -// } +// // Relation type is in clear, it might be possible to do some things? +// // Notice that if the event is decrypted later, process be called again +// val encryptedEventContent = event.content.toModel() +// when (encryptedEventContent?.relatesTo?.type) { +// RelationType.REPLACE -> { +// Timber.v("###REPLACE in room $roomId for event ${event.eventId}") +// // A replace! +// handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) +// } +// RelationType.RESPONSE -> { +// // can we / should we do we something for UTD response?? +// Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") +// } +// RelationType.REFERENCE -> { +// // can we / should we do we something for UTD reference?? +// Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") +// } +// RelationType.ANNOTATION -> { +// // can we / should we do we something for UTD annotation?? +// Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") +// } // } } EventType.REDACTION -> { @@ -217,9 +197,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( when (eventToPrune.type) { EventType.MESSAGE -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") -// val unsignedData = EventMapper.map(eventToPrune).unsignedData -// ?: UnsignedData(null, null) - // was this event a m.replace val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { @@ -236,7 +213,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) + handleReplace(realm, event, roomId, isLocalEcho, content?.relatesTo.eventId) } } in EventType.POLL_RESPONSE -> { @@ -274,23 +251,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private fun handleReplace( realm: Realm, event: Event, - content: MessageContent, roomId: String, isLocalEcho: Boolean, - relatedEventId: String? = null + relatedEventId: String? ) { val eventId = event.eventId ?: return - val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return - val newContent = content.newContent ?: return - - // Check that the sender is the same + val targetEventId = relatedEventId ?: return // ?: content.relatesTo?.eventId ?: return val editedEvent = EventEntity.where(realm, targetEventId).findFirst() - if (editedEvent == null) { - // We do not know yet about the edited event - } else if (editedEvent.sender != event.senderId) { - // Edited by someone else, ignore - Timber.w("Ignore edition by someone else") - return + + when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) { + is EventEditValidator.EditValidity.Invalid -> return Unit.also { + Timber.w("Dropping invalid edit ${event.eventId}, reason:${validity.reason}") + } + EventEditValidator.EditValidity.Unknown, // we can't drop the source event might be unknown, will be validated later + EventEditValidator.EditValidity.Valid -> { + // continue + } } // ok, this is a replace @@ -305,11 +281,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( .also { editSummary -> editSummary.editions.add( EditionOfEvent( - senderId = event.senderId ?: "", eventId = event.eventId, - content = ContentMapper.map(newContent), - timestamp = if (isLocalEcho) 0 else event.originServerTs ?: 0, - isLocalEcho = isLocalEcho + event = EventEntity.where(realm, eventId).findFirst(), + timestamp = if (isLocalEcho) clock.epochMillis() else event.originServerTs ?: clock.epochMillis(), + isLocalEcho = isLocalEcho, ) ) } @@ -334,9 +309,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") existingSummary.editions.add( EditionOfEvent( - senderId = event.senderId ?: "", eventId = event.eventId, - content = ContentMapper.map(newContent), + event = EventEntity.where(realm, eventId).findFirst(), timestamp = if (isLocalEcho) { clock.epochMillis() } else { @@ -369,8 +343,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( * @param editions list of edition of event */ private fun handleThreadSummaryEdition( - editedEvent: EventEntity?, - replaceEvent: TimelineEventEntity?, + editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?, editions: List? ) { replaceEvent ?: return @@ -599,12 +572,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) { event.getClearContent().toModel(catchError = true)?.let { liveLocationAggregationProcessor.handleBeaconLocationData( - realm, - event, - it, - roomId, - event.getRelationContent()?.eventId, - isLocalEcho + realm = realm, + event = event, + content = it, + roomId = roomId, + relatedEventId = event.getRelationContent()?.eventId, + isLocalEcho = isLocalEcho ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt index 03c2b2a47e..0cda6eca99 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt @@ -21,6 +21,7 @@ import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.Membership @@ -95,7 +96,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( * Create a local room entity from the given room creation params. * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room. */ - private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) { + private fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) { RoomEntity.getOrCreate(realm, roomId).apply { membership = Membership.JOIN chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody)) @@ -148,13 +149,16 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( * * @return a chunk entity */ - private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity { + private fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity { val chunkEntity = realm.createObject().apply { isLastBackward = true isLastForward = true } - val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody)) + // Can't suspend when using realm as it could jump thread + val eventList = runBlocking { + createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody)) + } val roomMemberContentsByUser = HashMap() for (event in eventList) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt index eb966b684c..8b5fde6ab7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt @@ -30,7 +30,7 @@ import javax.inject.Inject internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor { - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { val createRoomContent = event.getClearContent().toModel() val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt index fa3479ed3c..9041ef2677 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt @@ -40,7 +40,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) { return } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index cc86679cbc..7968eabd30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -46,7 +46,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr return eventType == EventType.REDACTION } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { pruneEvent(realm, event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt index 2b404775f0..3684bec167 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -30,7 +30,7 @@ import javax.inject.Inject internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { if (event.roomId == null) return val createRoomContent = event.getClearContent().toModel() if (createRoomContent?.replacementRoomId == null) return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt index 6152eacae5..af3ba80fe4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt @@ -22,7 +22,7 @@ import io.realm.RealmModel import org.matrix.android.sdk.internal.database.awaitTransaction import java.util.concurrent.atomic.AtomicReference -internal suspend fun Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { +internal suspend fun Monarchy.awaitTransaction(transaction: (realm: Realm) -> T): T { return awaitTransaction(realmConfiguration, transaction) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt new file mode 100644 index 0000000000..99942d967e --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room + +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore + +class EditValidationTest { + + private val mockTextEvent = Event( + type = EventType.MESSAGE, + eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + originServerTs = 1000, + senderId = "@alice:example.com", + ) + + private val mockEdit = Event( + type = EventType.MESSAGE, + eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.new_content" to mapOf( + "body" to "some message edited", + "msgtype" to "m.text" + ), + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ) + + @Test + fun `edit should be valid`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit(mockTextEvent, mockEdit) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + } + + @Test + fun `original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(senderId = "@bob:example.com") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `original event and replacement event must have the same room_id`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(roomId = "!someotherroom") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy(roomId = "!someotherroom") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `replacement and original events must not have a state_key property`() { + val mockCryptoStore = mockk() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(stateKey = "") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + mockTextEvent.copy(stateKey = ""), + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `replacement event must have an new_content property`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit(mockTextEvent, mockEdit.copy( + content = mockEdit.content!!.toMutableMap().apply { + this.remove("m.new_content") + } + )) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + ) + ) + } + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `The original event must not itself have a rel_type of m_replace`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent.copy( + content = mockTextEvent.content!!.toMutableMap().apply { + this["m.relates_to"] = mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + } + ), + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text", + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ), + ) + ) + }, + encryptedEditEvent + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `valid e2ee edit`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent + ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + } + + @Test + fun `If the original event was encrypted, the replacement should be too`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `encrypted, original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + mockk { + every { userId } returns "@bob:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + // if sent fom a deleted device it should use the event claimed sender id + } + + @Test + fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + null + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy( + senderId = "bob@example.com" + ).apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + private val encryptedEditEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ", + "device_id" to "QDHBLWOTSN", + "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG...deLfCQOSPunSSNDFdWuDkB8Cg", + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.new_content" to mapOf( + "body" to "some message edited", + "msgtype" to "m.text" + ), + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + ), + senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + isSafe = true + ) + } + + private val encryptedEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ", + "device_id" to "QDHBLWOTSN", + "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG+4Vr...Yf0gYyhVWZY4SedF3fTMwkjmTuel4fwrmq", + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + isSafe = true + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 93999458c6..87868e91d1 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -38,7 +38,7 @@ internal class FakeMonarchy { init { mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") coEvery { - instance.awaitTransaction(any Any>()) + instance.awaitTransaction(any<(Realm) -> Any>()) } coAnswers { secondArg Any>().invoke(fakeRealm.instance) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index 15a9823c79..38b67b5261 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -33,7 +33,7 @@ internal class FakeRealmConfiguration { val instance = mockk() fun givenAwaitTransaction(realm: Realm) { - val transaction = slot T>() + val transaction = slot<(Realm) -> T>() coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { secondArg T>().invoke(realm) } diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt index 5c3393416b..d907f39ee3 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt @@ -19,6 +19,7 @@ package im.vector.app.core.extensions import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.send.SendState @@ -40,7 +41,8 @@ fun TimelineEvent.getVectorLastMessageContent(): MessageContent? { // Iterate on event types which are not part of the matrix sdk, otherwise fallback to the sdk method return when (root.getClearType()) { VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { - (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + (annotations?.editSummary?.latestEdit?.getClearContent()?.toModel().toContent().toModel() + ?: root.getClearContent().toModel()) } else -> getLastMessageContent() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 57ad4331ce..1f079e420b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -442,6 +442,7 @@ class TimelineEventController @Inject constructor( val timelineEventsGroup = timelineEventsGroups.getOrNull(event) val params = TimelineItemFactoryParams( event = event, + lastEdit = event.annotations?.editSummary?.latestEdit, prevEvent = prevEvent, prevDisplayableEvent = prevDisplayableEvent, nextEvent = nextEvent, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 373410775b..d5d38f47a3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -161,7 +161,7 @@ class MessageItemFactory @Inject constructor( val callback = params.callback event.root.eventId ?: return null roomId = event.roomId - val informationData = messageInformationDataFactory.create(params) + val informationData = messageInformationDataFactory.create(params, params.event.annotations?.editSummary?.latestEdit) val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails if (event.root.isRedacted()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index 7c02b6f058..dd4494a613 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -19,10 +19,12 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent data class TimelineItemFactoryParams( val event: TimelineEvent, + val lastEdit: Event? = null, val prevEvent: TimelineEvent? = null, val prevDisplayableEvent: TimelineEvent? = null, val nextEvent: TimelineEvent? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 50b4366e98..a969c294f5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -31,11 +31,13 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.VerificationState +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.getMsgType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -55,7 +57,7 @@ class MessageInformationDataFactory @Inject constructor( private val reactionsSummaryFactory: ReactionsSummaryFactory ) { - fun create(params: TimelineItemFactoryParams): MessageInformationData { + fun create(params: TimelineItemFactoryParams, lastEdit: Event? = null): MessageInformationData { val event = params.event val nextDisplayableEvent = params.nextDisplayableEvent val prevDisplayableEvent = params.prevDisplayableEvent @@ -72,8 +74,14 @@ class MessageInformationDataFactory @Inject constructor( prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) - val e2eDecoration = getE2EDecoration(roomSummary, event) - + val e2eDecoration = getE2EDecoration(roomSummary, lastEdit ?: event.root) + val senderId = if (event.isEncrypted()) { + event.root.toValidDecryptedEvent()?.let { + session.cryptoService().deviceWithIdentityKey(it.cryptoSenderKey, it.algorithm)?.userId + } ?: event.root.senderId.orEmpty() + } else { + event.root.senderId.orEmpty() + } // SendState Decoration val sendStateDecoration = if (isSentByMe) { getSendStateDecoration( @@ -89,7 +97,7 @@ class MessageInformationDataFactory @Inject constructor( return MessageInformationData( eventId = eventId, - senderId = event.root.senderId ?: "", + senderId = senderId, sendState = event.root.sendState, time = time, ageLocalTS = event.root.ageLocalTs, @@ -148,34 +156,34 @@ class MessageInformationDataFactory @Inject constructor( } } - private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { + private fun getE2EDecoration(roomSummary: RoomSummary?, event: Event): E2EDecoration { if (roomSummary?.isEncrypted != true) { // No decoration for clear room // Questionable? what if the event is E2E? return E2EDecoration.NONE } - if (event.root.sendState != SendState.SYNCED) { + if (event.sendState != SendState.SYNCED) { // we don't display e2e decoration if event not synced back return E2EDecoration.NONE } val userCrossSigningInfo = session.cryptoService() .crossSigningService() - .getUserCrossSigningKeys(event.root.senderId.orEmpty()) + .getUserCrossSigningKeys(event.senderId.orEmpty()) if (userCrossSigningInfo?.isTrusted() == true) { return if (event.isEncrypted()) { // Do not decorate failed to decrypt, or redaction (we lost sender device info) - if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { + if (event.getClearType() == EventType.ENCRYPTED || event.isRedacted()) { E2EDecoration.NONE } else { - val sendingDevice = event.root.getSenderKey() + val sendingDevice = event.getSenderKey() ?.let { session.cryptoService().deviceWithIdentityKey( it, - event.root.content?.get("algorithm") as? String ?: "" + event.content?.get("algorithm") as? String ?: "" ) } - if (event.root.mxDecryptionResult?.isSafe == false) { + if (event.mxDecryptionResult?.isSafe == false) { E2EDecoration.WARN_UNSAFE_KEY } else { when { @@ -202,8 +210,8 @@ class MessageInformationDataFactory @Inject constructor( } else { return if (!event.isEncrypted()) { e2EDecorationForClearEventInE2ERoom(event, roomSummary) - } else if (event.root.mxDecryptionResult != null) { - if (event.root.mxDecryptionResult?.isSafe == true) { + } else if (event.mxDecryptionResult != null) { + if (event.mxDecryptionResult?.isSafe == true) { E2EDecoration.NONE } else { E2EDecoration.WARN_UNSAFE_KEY @@ -214,13 +222,13 @@ class MessageInformationDataFactory @Inject constructor( } } - private fun e2EDecorationForClearEventInE2ERoom(event: TimelineEvent, roomSummary: RoomSummary) = - if (event.root.isStateEvent()) { + private fun e2EDecorationForClearEventInE2ERoom(event: Event, roomSummary: RoomSummary) = + if (event.isStateEvent()) { // Do not warn for state event, they are always in clear E2EDecoration.NONE } else { val ts = roomSummary.encryptionEventTs ?: 0 - val eventTs = event.root.originServerTs ?: 0 + val eventTs = event.originServerTs ?: 0 // If event is in clear after the room enabled encryption we should warn if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE } From e66a0541bea50e1e8d320b657a6bfd5f882c3599 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 16 Nov 2022 10:56:10 +0100 Subject: [PATCH 297/679] Add changelog, some cleaning --- changelog.d/7594.misc | 1 + .../sdk/internal/database/migration/MigrateSessionTo043.kt | 2 +- .../android/sdk/internal/session/room/EventEditValidator.kt | 6 ++++-- .../session/room/EventRelationsAggregationProcessor.kt | 4 ++-- .../android/sdk/internal/session/room/EditValidationTest.kt | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 changelog.d/7594.misc diff --git a/changelog.d/7594.misc b/changelog.d/7594.misc new file mode 100644 index 0000000000..5c5771d8d0 --- /dev/null +++ b/changelog.d/7594.misc @@ -0,0 +1 @@ +Better validation of edits diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt index a27d4fda3a..d11a671c55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt index dcf6ad54a0..940da25f11 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -33,13 +33,15 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto } /** - *There are a number of requirements on replacement events, which must be satisfied for the replacement to be considered valid: + * There are a number of requirements on replacement events, which must be satisfied for the replacement + * to be considered valid: * As with all event relationships, the original event and replacement event must have the same room_id * (i.e. you cannot send an event in one room and then an edited version in a different room). * The original event and replacement event must have the same sender (i.e. you cannot edit someone else’s messages). * The replacement and original events must have the same type (i.e. you cannot change the original event’s type). * The replacement and original events must not have a state_key property (i.e. you cannot edit state events at all). - * The original event must not, itself, have a rel_type of m.replace (i.e. you cannot edit an edit — though you can send multiple edits for a single original event). + * The original event must not, itself, have a rel_type of m.replace + * (i.e. you cannot edit an edit — though you can send multiple edits for a single original event). * The replacement event (once decrypted, if appropriate) must have an m.new_content property. * * If the original event was encrypted, the replacement should be too. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index ef1d8c1430..837d00720b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -213,7 +213,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! - handleReplace(realm, event, roomId, isLocalEcho, content?.relatesTo.eventId) + handleReplace(realm, event, roomId, isLocalEcho, content.relatesTo.eventId) } } in EventType.POLL_RESPONSE -> { @@ -474,7 +474,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val sourceToDiscard = eventSummary.editSummary?.editions?.firstOrNull { it.eventId == redacted.eventId } if (sourceToDiscard == null) { - Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") + Timber.w("Redaction of a replace that was not known in aggregation") return } // Need to remove this event from the edition list diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt index 99942d967e..429e6625ab 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 3746ede49a8b2b4d1ac6eff96e26b1f21d233d1a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 16 Nov 2022 11:34:08 +0100 Subject: [PATCH 298/679] Fix test --- .../session/room/send/LocalEchoEventFactoryTests.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt index b30428e5e1..19f58d690f 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt @@ -23,8 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary -import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.sender.SenderInfo @@ -226,16 +224,14 @@ class LocalEchoEventFactoryTests { ).toMessageTextContent().toContent() } return TimelineEvent( - root = A_START_EVENT, + root = A_START_EVENT.copy( + type = EventType.MESSAGE, + content = textContent + ), localId = 1234, eventId = AN_EVENT_ID, displayIndex = 0, senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null), - annotations = if (textContent != null) { - EventAnnotationsSummary( - editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList()) - ) - } else null ) } } From 8b47bf004ee34292db71c1b8a3a75769c46956ae Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 16 Nov 2022 13:45:52 +0100 Subject: [PATCH 299/679] Fix broken polls states --- .../session/room/timeline/TimelineEvent.kt | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 6ceced59d7..6b4a0226a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.timeline import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType @@ -30,8 +31,12 @@ import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.sender.SenderInfo @@ -136,12 +141,21 @@ fun TimelineEvent.getEditedEventId(): String? { * Get last MessageContent, after a possible edition. */ fun TimelineEvent.getLastMessageContent(): MessageContent? { - return ( - annotations?.editSummary?.latestEdit - ?.getClearContent()?.toModel()?.newContent - ?: root.getClearContent() - ) - .toModel() + return when (root.getClearType()) { + EventType.STICKER -> root.getClearContent().toModel() + // XXX + // Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing + // so toModel won't parse them correctly + // It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion? + in EventType.POLL_START -> (getLastEditNewContent() ?: root.getClearContent()).toModel() + in EventType.STATE_ROOM_BEACON_INFO -> (getLastEditNewContent() ?: root.getClearContent()).toModel() + in EventType.BEACON_LOCATION_DATA -> (getLastEditNewContent() ?: root.getClearContent()).toModel() + else -> (getLastEditNewContent() ?: root.getClearContent()).toModel() + } +} + +fun TimelineEvent.getLastEditNewContent(): Content? { + return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent } /** From d759f26db6756850e021f4d6aa313f190c7178c5 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 16 Nov 2022 14:10:37 +0100 Subject: [PATCH 300/679] fix fake awaitTx --- .../java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt | 4 ++-- .../matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 87868e91d1..76ede75910 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -39,8 +39,8 @@ internal class FakeMonarchy { mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") coEvery { instance.awaitTransaction(any<(Realm) -> Any>()) - } coAnswers { - secondArg Any>().invoke(fakeRealm.instance) + } answers { + secondArg<(Realm) -> Any>().invoke(fakeRealm.instance) } coEvery { instance.doWithRealm(any()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index 38b67b5261..f5545b7e76 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -34,8 +34,8 @@ internal class FakeRealmConfiguration { fun givenAwaitTransaction(realm: Realm) { val transaction = slot<(Realm) -> T>() - coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { - secondArg T>().invoke(realm) + coEvery { awaitTransaction(instance, capture(transaction)) } answers { + secondArg<(Realm) -> T>().invoke(realm) } } } From e5d3206b6ff4392f5280d257529761afa0a2ad1b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 23 Nov 2022 19:08:17 +0100 Subject: [PATCH 301/679] code review --- .../events/model/AggregatedRelations.kt | 4 +- .../events/model/ValidDecryptedEvent.kt | 26 +--- .../EditAggregatedSummaryEntityMapper.kt | 45 ++++++ .../mapper/EventAnnotationsSummaryMapper.kt | 24 +--- .../database/migration/MigrateSessionTo008.kt | 6 +- .../sdk/internal/session/events/EventExt.kt | 30 ++++ .../session/room/EventEditValidator.kt | 15 +- .../EventRelationsAggregationProcessor.kt | 58 ++++---- .../EditAggregationSummaryMapperTest.kt | 78 ++++++++++ .../session/event/ValidDecryptedEventTest.kt | 134 ++++++++++++++++++ ...ationTest.kt => EventEditValidatorTest.kt} | 18 ++- .../sdk/test/fakes/FakeRealmConfiguration.kt | 4 +- .../timeline/factory/MessageItemFactory.kt | 2 +- .../helper/MessageInformationDataFactory.kt | 22 +-- 14 files changed, 366 insertions(+), 100 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt rename matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/{EditValidationTest.kt => EventEditValidatorTest.kt} (96%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt index 1dedcce8b6..7f043dc0f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -19,7 +19,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** - * + * ``` * { * "m.annotation": { * "chunk": [ @@ -43,7 +43,7 @@ import com.squareup.moshi.JsonClass * "count": 1 * } * } - * + * ``` */ @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt index 0cee077807..b305bf19b0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.api.session.events.model +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + data class ValidDecryptedEvent( val type: String, val eventId: String, @@ -29,25 +32,6 @@ data class ValidDecryptedEvent( val algorithm: String, ) -fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { - if (!this.isEncrypted()) return null - val decryptedContent = this.getDecryptedContent() ?: return null - val eventId = this.eventId ?: return null - val roomId = this.roomId ?: return null - val type = this.getDecryptedType() ?: return null - val senderKey = this.getSenderKey() ?: return null - val algorithm = this.content?.get("algorithm") as? String ?: return null - - return ValidDecryptedEvent( - type = type, - eventId = eventId, - clearContent = decryptedContent, - prevContent = this.prevContent, - originServerTs = this.originServerTs ?: 0, - cryptoSenderKey = senderKey, - roomId = roomId, - unsignedData = this.unsignedData, - redacts = this.redacts, - algorithm = algorithm - ) +fun ValidDecryptedEvent.getRelationContent(): RelationDefaultContent? { + return clearContent.toModel()?.relatesTo } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt new file mode 100644 index 0000000000..8c209f2f2a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EditionOfEvent + +internal object EditAggregatedSummaryEntityMapper { + + fun map(summary: EditAggregatedSummaryEntity?): EditAggregatedSummary? { + summary ?: return null + /** + * The most recent event is determined by comparing origin_server_ts; + * if two or more replacement events have identical origin_server_ts, + * the event with the lexicographically largest event_id is treated as more recent. + */ + val latestEdition = summary.editions.sortedWith(compareBy { it.timestamp }.thenBy { it.eventId }) + .lastOrNull() ?: return null + val editEvent = latestEdition.event + + return EditAggregatedSummary( + latestEdit = editEvent?.asDomain(), + sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } + .map { editionOfEvent -> editionOfEvent.eventId }, + localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } + .map { editionOfEvent -> editionOfEvent.eventId }, + lastEditTs = latestEdition.timestamp + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 5fb70ad1ee..d4bb5791a0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -16,11 +16,9 @@ package org.matrix.android.sdk.internal.database.mapper -import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary -import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity internal object EventAnnotationsSummaryMapper { @@ -36,27 +34,7 @@ internal object EventAnnotationsSummaryMapper { it.sourceLocalEcho.toList() ) }, - editSummary = annotationsSummary.editSummary - ?.let { summary -> - /** - * The most recent event is determined by comparing origin_server_ts; - * if two or more replacement events have identical origin_server_ts, - * the event with the lexicographically largest event_id is treated as more recent. - */ - val latestEdition = summary.editions.sortedWith(compareBy { it.timestamp }.thenBy { it.eventId }) - .lastOrNull() ?: return@let null - // get the event and validate? - val editEvent = latestEdition.event - - EditAggregatedSummary( - latestEdit = editEvent?.asDomain(), - sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } - .map { editionOfEvent -> editionOfEvent.eventId }, - localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } - .map { editionOfEvent -> editionOfEvent.eventId }, - lastEditTs = latestEdition.timestamp - ) - }, + editSummary = EditAggregatedSummaryEntityMapper.map(annotationsSummary.editSummary), referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let { ReferencesAggregatedSummary( ContentMapper.map(it.content), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt index 42a47a9a27..f85a0661c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt @@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8 override fun doMigrate(realm: DynamicRealm) { val editionOfEventSchema = realm.schema.create("EditionOfEvent") - .addField("content"/**EditionOfEventFields.CONTENT*/, String::class.java) + .addField("content", String::class.java) .addField(EditionOfEventFields.EVENT_ID, String::class.java) .setRequired(EditionOfEventFields.EVENT_ID, true) - .addField("senderId" /*EditionOfEventFields.SENDER_ID*/, String::class.java) - .setRequired("senderId" /*EditionOfEventFields.SENDER_ID*/, true) + .addField("senderId", String::class.java) + .setRequired("senderId", true) .addField(EditionOfEventFields.TIMESTAMP, Long::class.java) .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt index 91e709e464..63409a15bb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.events import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.ValidDecryptedEvent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent @@ -33,3 +34,32 @@ internal fun Event.getFixedRoomMemberContent(): RoomMemberContent? { content } } + +fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { + if (!this.isEncrypted()) return null + val decryptedContent = this.getDecryptedContent() ?: return null + val eventId = this.eventId ?: return null + val roomId = this.roomId ?: return null + val type = this.getDecryptedType() ?: return null + val senderKey = this.getSenderKey() ?: return null + val algorithm = this.content?.get("algorithm") as? String ?: return null + + // copy the relation as it's in clear in the encrypted content + val updatedContent = this.content.get("m.relates_to")?.let { + decryptedContent.toMutableMap().apply { + put("m.relates_to", it) + } + } ?: decryptedContent + return ValidDecryptedEvent( + type = type, + eventId = eventId, + clearContent = updatedContent, + prevContent = this.prevContent, + originServerTs = this.originServerTs ?: 0, + cryptoSenderKey = senderKey, + roomId = roomId, + unsignedData = this.unsignedData, + redacts = this.redacts, + algorithm = algorithm + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt index 940da25f11..ea14aacfe4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -17,11 +17,14 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent +import timber.log.Timber import javax.inject.Inject internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) { @@ -47,12 +50,18 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto * If the original event was encrypted, the replacement should be too. */ fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity { + Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent") // we might not know the original event at that time. In this case we can't perform the validation // Edits should be revalidated when the original event is received if (originalEvent == null) { return EditValidity.Unknown } + if (LocalEcho.isLocalEchoId(replaceEvent.eventId.orEmpty())) { + // Don't validate local echo + return EditValidity.Unknown + } + if (originalEvent.roomId != replaceEvent.roomId) { return EditValidity.Invalid("original event and replacement event must have the same room_id") } @@ -71,7 +80,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId - if (originalDecrypted.clearContent.toModel()?.relatesTo?.type == RelationType.REPLACE) { + if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) { return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") } @@ -96,7 +105,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto return EditValidity.Invalid("replacement event must have an m.new_content property") } } else { - if (originalEvent.content.toModel()?.relatesTo?.type == RelationType.REPLACE) { + if (originalEvent.getRelationContent()?.type == RelationType.REPLACE) { return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 837d00720b..48e75821bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -81,13 +82,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.REDACTION, EventType.REACTION, // The aggregator handles verification events but just to render tiles in the timeline - // It's not participating in verfication it's self, just timeline display + // It's not participating in verification itself, just timeline display EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, - // TODO Add ? EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED @@ -168,28 +168,28 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // As for now Live event processors are not receiving UTD events. // They will get an update if the event is decrypted later EventType.ENCRYPTED -> { -// // Relation type is in clear, it might be possible to do some things? -// // Notice that if the event is decrypted later, process be called again -// val encryptedEventContent = event.content.toModel() -// when (encryptedEventContent?.relatesTo?.type) { -// RelationType.REPLACE -> { -// Timber.v("###REPLACE in room $roomId for event ${event.eventId}") -// // A replace! -// handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) -// } -// RelationType.RESPONSE -> { -// // can we / should we do we something for UTD response?? -// Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") -// } -// RelationType.REFERENCE -> { -// // can we / should we do we something for UTD reference?? -// Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") -// } -// RelationType.ANNOTATION -> { -// // can we / should we do we something for UTD annotation?? -// Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") -// } -// } + // Relation type is in clear, it might be possible to do some things? + // Notice that if the event is decrypted later, process be called again + val encryptedEventContent = event.content.toModel() + when (encryptedEventContent?.relatesTo?.type) { + RelationType.REPLACE -> { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } + RelationType.RESPONSE -> { + // can we / should we do we something for UTD response?? + Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + RelationType.REFERENCE -> { + // can we / should we do we something for UTD reference?? + Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + RelationType.ANNOTATION -> { + // can we / should we do we something for UTD annotation?? + Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + } } EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } @@ -256,7 +256,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( relatedEventId: String? ) { val eventId = event.eventId ?: return - val targetEventId = relatedEventId ?: return // ?: content.relatesTo?.eventId ?: return + val targetEventId = relatedEventId ?: return val editedEvent = EventEntity.where(realm, targetEventId).findFirst() when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) { @@ -301,15 +301,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // ok it has already been managed Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") existingSummary.editions.firstOrNull { it.eventId == txId }?.let { - it.eventId = event.eventId + it.eventId = eventId it.timestamp = event.originServerTs ?: clock.epochMillis() it.isLocalEcho = false + it.event = EventEntity.where(realm, eventId).findFirst() } } else { Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") existingSummary.editions.add( EditionOfEvent( - eventId = event.eventId, + eventId = eventId, event = EventEntity.where(realm, eventId).findFirst(), timestamp = if (isLocalEcho) { clock.epochMillis() @@ -343,7 +344,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( * @param editions list of edition of event */ private fun handleThreadSummaryEdition( - editedEvent: EventEntity?, replaceEvent: TimelineEventEntity?, + editedEvent: EventEntity?, + replaceEvent: TimelineEventEntity?, editions: List? ) { replaceEvent ?: return diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt new file mode 100644 index 0000000000..12ff9c1d37 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import io.mockk.every +import io.mockk.mockk +import io.realm.RealmList +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBe +import org.junit.Test +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EditionOfEvent +import org.matrix.android.sdk.internal.database.model.EventEntity + +class EditAggregationSummaryMapperTest { + + @Test + fun test() { + val edits = RealmList( + EditionOfEvent( + timestamp = 0L, + eventId = "e0", + isLocalEcho = false, + event = mockEvent("e0") + ), + EditionOfEvent( + timestamp = 1L, + eventId = "e1", + isLocalEcho = false, + event = mockEvent("e1") + ), + EditionOfEvent( + timestamp = 30L, + eventId = "e2", + isLocalEcho = true, + event = mockEvent("e2") + ) + ) + val fakeSummaryEntity = mockk { + every { editions } returns edits + } + + val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) + mapped shouldNotBe null + mapped!!.sourceEvents.size shouldBe 2 + mapped.localEchos.size shouldBe 1 + mapped.localEchos.first() shouldBe "e2" + + mapped.lastEditTs shouldBe 30L + mapped.latestEdit?.eventId shouldBe "e2" + } + + private fun mockEvent(eventId: String): EventEntity { + return EventEntity().apply { + this.eventId = eventId + this.content = """ + { + "body" : "Hello", + "msgtype": "text" + } + """.trimIndent() + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt new file mode 100644 index 0000000000..8dead42f60 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.event + +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBe +import org.junit.Test +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent + +class ValidDecryptedEventTest { + + val fakeEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventId", + roomId = "!fakeRoom", + content = EncryptedEventContent( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + sessionId = "TO2G4u2HlnhtbIJk", + senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + deviceId = "FAKEE" + ).toContent() + ) + + @Test + fun `A failed to decrypt message should give a null validated decrypted event`() { + fakeEvent.toValidDecryptedEvent() shouldBe null + } + + @Test + fun `Mismatch sender key detection`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + val validDecryptedEvent = decryptedEvent.toValidDecryptedEvent() + validDecryptedEvent shouldNotBe null + + fakeEvent.content!!["senderKey"] shouldNotBe "the_real_sender_key" + validDecryptedEvent!!.cryptoSenderKey shouldBe "the_real_sender_key" + } + + @Test + fun `Mixed content event should be detected`() { + val mixedEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventd ", + roomId = "!fakeRoo", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "ciphertext" to "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + "sessionId" to "TO2G4u2HlnhtbIJk", + "senderKey" to "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + "deviceId" to "FAKEE", + "body" to "some message", + "msgtype" to "m.text" + ).toContent() + ) + + val unValidatedContent = mixedEvent.getClearContent().toModel() + unValidatedContent?.body shouldBe "some message" + + mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel() shouldBe null + } + + @Test + fun `Basic field validation`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + decryptedEvent.toValidDecryptedEvent() shouldNotBe null + decryptedEvent.copy(roomId = null).toValidDecryptedEvent() shouldBe null + decryptedEvent.copy(eventId = null).toValidDecryptedEvent() shouldBe null + } + + @Test + fun `A clear event is not a valid decrypted event`() { + val mockTextEvent = Event( + type = EventType.MESSAGE, + eventId = "eventId", + roomId = "!fooe:example.com", + content = mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + originServerTs = 1000, + senderId = "@anne:example.com", + ) + mockTextEvent.toValidDecryptedEvent() shouldBe null + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt similarity index 96% rename from matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt index 429e6625ab..0ae712bff1 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EditValidationTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt @@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -class EditValidationTest { +class EventEditValidatorTest { private val mockTextEvent = Event( type = EventType.MESSAGE, @@ -180,17 +180,23 @@ class EditValidationTest { validator .validateEdit( - encryptedEvent.copy().apply { + encryptedEvent.copy( + content = encryptedEvent.content!!.toMutableMap().apply { + put( + "m.relates_to", + mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + } + ).apply { mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( payload = mapOf( "type" to EventType.MESSAGE, "content" to mapOf( "body" to "some message", "msgtype" to "m.text", - "m.relates_to" to mapOf( - "rel_type" to "m.replace", - "event_id" to mockTextEvent.eventId - ) ), ) ) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index f5545b7e76..9ad7032262 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.slot import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.database.awaitTransaction @@ -33,8 +32,7 @@ internal class FakeRealmConfiguration { val instance = mockk() fun givenAwaitTransaction(realm: Realm) { - val transaction = slot<(Realm) -> T>() - coEvery { awaitTransaction(instance, capture(transaction)) } answers { + coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers { secondArg<(Realm) -> T>().invoke(realm) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index d5d38f47a3..373410775b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -161,7 +161,7 @@ class MessageItemFactory @Inject constructor( val callback = params.callback event.root.eventId ?: return null roomId = event.roomId - val informationData = messageInformationDataFactory.create(params, params.event.annotations?.editSummary?.latestEdit) + val informationData = messageInformationDataFactory.create(params) val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails if (event.root.isRedacted()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index a969c294f5..3c8b342a1a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -37,7 +37,6 @@ import org.matrix.android.sdk.api.session.events.model.getMsgType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited +import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent import javax.inject.Inject /** @@ -57,7 +57,7 @@ class MessageInformationDataFactory @Inject constructor( private val reactionsSummaryFactory: ReactionsSummaryFactory ) { - fun create(params: TimelineItemFactoryParams, lastEdit: Event? = null): MessageInformationData { + fun create(params: TimelineItemFactoryParams): MessageInformationData { val event = params.event val nextDisplayableEvent = params.nextDisplayableEvent val prevDisplayableEvent = params.prevDisplayableEvent @@ -74,14 +74,8 @@ class MessageInformationDataFactory @Inject constructor( prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) - val e2eDecoration = getE2EDecoration(roomSummary, lastEdit ?: event.root) - val senderId = if (event.isEncrypted()) { - event.root.toValidDecryptedEvent()?.let { - session.cryptoService().deviceWithIdentityKey(it.cryptoSenderKey, it.algorithm)?.userId - } ?: event.root.senderId.orEmpty() - } else { - event.root.senderId.orEmpty() - } + val e2eDecoration = getE2EDecoration(roomSummary, params.lastEdit ?: event.root) + val senderId = getSenderId(event) // SendState Decoration val sendStateDecoration = if (isSentByMe) { getSendStateDecoration( @@ -139,6 +133,14 @@ class MessageInformationDataFactory @Inject constructor( ) } + private fun getSenderId(event: TimelineEvent) = if (event.isEncrypted()) { + event.root.toValidDecryptedEvent()?.let { + session.cryptoService().deviceWithIdentityKey(it.cryptoSenderKey, it.algorithm)?.userId + } ?: event.root.senderId.orEmpty() + } else { + event.root.senderId.orEmpty() + } + private fun getSendStateDecoration( event: TimelineEvent, lastSentEventWithoutReadReceipts: String?, From 2819957585a1a70a1a3cea2636f38dd73040b2ae Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 23 Nov 2022 23:45:35 +0100 Subject: [PATCH 302/679] fix edit display flicker with local echo --- .../sync/handler/room/RoomSyncHandler.kt | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 2825be8291..4001ae2ccf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.room.model.Membership @@ -49,6 +51,7 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity @@ -486,23 +489,41 @@ internal class RoomSyncHandler @Inject constructor( cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) // Try to remove local echo - event.unsignedData?.transactionId?.also { - val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) + event.unsignedData?.transactionId?.also { txId -> + val sendingEventEntity = roomEntity.sendingTimelineEvents.find(txId) if (sendingEventEntity != null) { - Timber.v("Remove local echo for tx:$it") + Timber.v("Remove local echo for tx:$txId") roomEntity.sendingTimelineEvents.remove(sendingEventEntity) if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) { - // updated with echo decryption, to avoid seeing it decrypt again + // updated with echo decryption, to avoid seeing txId decrypt again val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) sendingEventEntity.root?.decryptionResultJson?.let { json -> eventEntity.decryptionResultJson = json event.mxDecryptionResult = adapter.fromJson(json) } } + // also update potential edit that could refer to that event? + // If not display will flicker :/ + val relationContent = event.getRelationContent() + if (relationContent?.type == RelationType.REPLACE) { + relationContent.eventId?.let { targetId -> + EventAnnotationsSummaryEntity.where(realm, roomId, targetId) + .findFirst() + ?.editSummary + ?.editions + ?.forEach { + if (it.eventId == txId) { + // just do that, the aggregation processor will to the rest + it.event = eventEntity + } + } + } + } + // Finally delete the local echo sendingEventEntity.deleteOnCascade(true) } else { - Timber.v("Can't find corresponding local echo for tx:$it") + Timber.v("Can't find corresponding local echo for tx:$txId") } } } From ca907df94b9fc3b2764b5972fc31c652498cd3d8 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 23 Nov 2022 23:47:45 +0100 Subject: [PATCH 303/679] kdoc fix --- .../android/sdk/api/session/events/model/AggregatedRelations.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt index 7f043dc0f7..6577a9b41e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -19,6 +19,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** + * Server side relation aggregation. * ``` * { * "m.annotation": { From c06eca69360f0bd2b180b04677133e44a46ba0a8 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 24 Nov 2022 01:38:03 +0100 Subject: [PATCH 304/679] Migration test and cleaning --- .../src/androidTest/assets/session_42.realm | Bin 0 -> 270336 bytes .../RealmSessionStoreMigration43Test.kt | 130 ++++++++++++ .../database/SessionSanityMigrationTest.kt | 64 ++++++ .../database/TestRealmConfigurationFactory.kt | 196 ++++++++++++++++++ .../sdk/api/session/events/model/EventExt.kt | 46 ++++ .../database/migration/MigrateSessionTo043.kt | 8 +- .../sdk/internal/session/events/EventExt.kt | 30 --- .../session/room/EventEditValidator.kt | 2 +- .../EditAggregationSummaryMapperTest.kt | 2 +- .../session/event/ValidDecryptedEventTest.kt | 4 +- .../helper/MessageInformationDataFactory.kt | 2 +- 11 files changed, 445 insertions(+), 39 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/assets/session_42.realm create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt diff --git a/matrix-sdk-android/src/androidTest/assets/session_42.realm b/matrix-sdk-android/src/androidTest/assets/session_42.realm new file mode 100644 index 0000000000000000000000000000000000000000..b92d13dab2fa6d44a38ee3443ae8cad85340db23 GIT binary patch literal 270336 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

~n*FN$6ddX2RG}!itYe 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%0DTX_=9PRTAUr6@kN4E#1dFf1 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 L00KbZ|0M7~<4-Vn literal 0 HcmV?d00001 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt new file mode 100644 index 0000000000..e74aa52495 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import org.amshove.kluent.fail +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBe +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.util.Normalizer + +@RunWith(AndroidJUnit4::class) +class RealmSessionStoreMigration43Test { + + @get:Rule val configurationFactory = TestRealmConfigurationFactory() + + lateinit var context: Context + var realm: Realm? = null + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + realm?.close() + } + + @Test + fun migrationShouldBeNeeed() { + val realmName = "session_42.realm" + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0", + SessionRealmModule(), + 43, + null + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + try { + realm = Realm.getInstance(realmConfiguration) + fail("Should need a migration") + } catch (failure: Throwable) { + // nop + } + } + + // Database key for alias `session_db_e00482619b2597069b1f192b86de7da9`: efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0 + // $WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI + // $11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo + @Test + fun testMigration43() { + val realmName = "session_42.realm" + val migration = RealmSessionStoreMigration(Normalizer()) + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0", + SessionRealmModule(), + 43, + migration + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + realm = Realm.getInstance(realmConfiguration) + + // assert that the edit from 42 are migrated + val editions = EventAnnotationsSummaryEntity + .where(realm!!, "\$WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI") + .findFirst() + ?.editSummary + ?.editions + + editions shouldNotBe null + editions!!.size shouldBe 1 + val firstEdition = editions.first() + firstEdition?.eventId shouldBeEqualTo "\$DvOyA8vJxwGfTaJG3OEJVcL4isShyaVDnprihy38W28" + firstEdition?.isLocalEcho shouldBeEqualTo false + + val editEvent = EventMapper.map(firstEdition!!.event!!) + val body = editEvent.content.toModel()?.body + body shouldBeEqualTo "* Message 2 with edit" + + // assert that the edit from 42 are migrated + val editionsOfE2E = EventAnnotationsSummaryEntity + .where(realm!!, "\$11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo") + .findFirst() + ?.editSummary + ?.editions + + editionsOfE2E shouldNotBe null + editionsOfE2E!!.size shouldBe 1 + val firstEditionE2E = editionsOfE2E.first() + firstEditionE2E?.eventId shouldBeEqualTo "\$HUwJOQRCJwfPv7XSKvBPcvncjM0oR3q2tGIIIdv9Zts" + firstEditionE2E?.isLocalEcho shouldBeEqualTo false + + val editEventE2E = EventMapper.map(firstEditionE2E!!.event!!) + val body2 = editEventE2E.getClearContent().toModel()?.body + body2 shouldBeEqualTo "* Message 2, e2e edit" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt new file mode 100644 index 0000000000..fc1a78835b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.util.Normalizer + +@RunWith(AndroidJUnit4::class) +class SessionSanityMigrationTest { + + @get:Rule val configurationFactory = TestRealmConfigurationFactory() + + lateinit var context: Context + var realm: Realm? = null + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + realm?.close() + } + + @Test + fun sessionDatabaseShouldMigrateGracefully() { + val realmName = "session_42.realm" + val migration = RealmSessionStoreMigration(Normalizer()) + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0", + SessionRealmModule(), + migration.schemaVersion, + migration + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + realm = Realm.getInstance(realmConfiguration) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt new file mode 100644 index 0000000000..fc5a017287 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.database + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmMigration +import org.junit.rules.TemporaryFolder +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.lang.IllegalStateException +import java.util.Collections +import java.util.Locale +import java.util.concurrent.ConcurrentHashMap +import kotlin.Throws + +/** + * Based on https://github.com/realm/realm-java/blob/master/realm/realm-library/src/testUtils/java/io/realm/TestRealmConfigurationFactory.java + */ +class TestRealmConfigurationFactory : TemporaryFolder() { + private val map: Map = ConcurrentHashMap() + private val configurations = Collections.newSetFromMap(map) + @get:Synchronized private var isUnitTestFailed = false + private var testName = "" + private var tempFolder: File? = null + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + setTestName(description) + before() + try { + base.evaluate() + } catch (throwable: Throwable) { + setUnitTestFailed() + throw throwable + } finally { + after() + } + } + } + } + + @Throws(Throwable::class) + override fun before() { + Realm.init(InstrumentationRegistry.getInstrumentation().targetContext) + super.before() + } + + override fun after() { + try { + for (configuration in configurations) { + Realm.deleteRealm(configuration) + } + } catch (e: IllegalStateException) { + // Only throws the exception caused by deleting the opened Realm if the test case itself doesn't throw. + if (!isUnitTestFailed) { + throw e + } + } finally { + // This will delete the temp directory. + super.after() + } + } + + @Throws(IOException::class) + override fun create() { + super.create() + tempFolder = File(super.getRoot(), testName) + check(!(tempFolder!!.exists() && !tempFolder!!.delete())) { "Could not delete folder: " + tempFolder!!.absolutePath } + check(tempFolder!!.mkdir()) { "Could not create folder: " + tempFolder!!.absolutePath } + } + + override fun getRoot(): File { + checkNotNull(tempFolder) { "the temporary folder has not yet been created" } + return tempFolder!! + } + + /** + * To be called in the [.apply]. + */ + protected fun setTestName(description: Description) { + testName = description.displayName + } + + @Synchronized + fun setUnitTestFailed() { + isUnitTestFailed = true + } + + // This builder creates a configuration that is *NOT* managed. + // You have to delete it yourself. + private fun createConfigurationBuilder(): RealmConfiguration.Builder { + return RealmConfiguration.Builder().directory(root) + } + + fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + fun createConfiguration( + name: String, + key: String?, + module: Any, + schemaVersion: Long, + migration: RealmMigration? + ): RealmConfiguration { + val builder = createConfigurationBuilder() + builder + .directory(root) + .name(name) + .apply { + if (key != null) { + encryptionKey(key.decodeHex()) + } + } + .modules(module) + // Allow writes on UI + .allowWritesOnUiThread(true) + .schemaVersion(schemaVersion) + .apply { + migration?.let { migration(it) } + } + val configuration = builder.build() + configurations.add(configuration) + return configuration + } + + // Copies a Realm file from assets to temp dir + @Throws(IOException::class) + fun copyRealmFromAssets(context: Context, realmPath: String, newName: String) { + val config = RealmConfiguration.Builder() + .directory(root) + .name(newName) + .build() + copyRealmFromAssets(context, realmPath, config) + } + + @Throws(IOException::class) + fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) { + check(!File(config.path).exists()) { String.format(Locale.ENGLISH, "%s exists!", config.path) } + val outFile = File(config.realmDirectory, config.realmFileName) + copyFileFromAssets(context, realmPath, outFile) + } + + @Throws(IOException::class) + fun copyFileFromAssets(context: Context, assetPath: String?, outFile: File?) { + var stream: InputStream? = null + var os: FileOutputStream? = null + try { + stream = context.assets.open(assetPath!!) + os = FileOutputStream(outFile) + val buf = ByteArray(1024) + var bytesRead: Int + while (stream.read(buf).also { bytesRead = it } > -1) { + os.write(buf, 0, bytesRead) + } + } finally { + if (stream != null) { + try { + stream.close() + } catch (ignore: IOException) { + } + } + if (os != null) { + try { + os.close() + } catch (ignore: IOException) { + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt new file mode 100644 index 0000000000..32d5ebed8c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.events.model + +fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { + if (!this.isEncrypted()) return null + val decryptedContent = this.getDecryptedContent() ?: return null + val eventId = this.eventId ?: return null + val roomId = this.roomId ?: return null + val type = this.getDecryptedType() ?: return null + val senderKey = this.getSenderKey() ?: return null + val algorithm = this.content?.get("algorithm") as? String ?: return null + + // copy the relation as it's in clear in the encrypted content + val updatedContent = this.content.get("m.relates_to")?.let { + decryptedContent.toMutableMap().apply { + put("m.relates_to", it) + } + } ?: decryptedContent + return ValidDecryptedEvent( + type = type, + eventId = eventId, + clearContent = updatedContent, + prevContent = this.prevContent, + originServerTs = this.originServerTs ?: 0, + cryptoSenderKey = senderKey, + roomId = roomId, + unsignedData = this.unsignedData, + redacts = this.redacts, + algorithm = algorithm + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt index d11a671c55..c7fda61671 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt @@ -18,8 +18,8 @@ package org.matrix.android.sdk.internal.database.migration import io.realm.DynamicRealm import org.matrix.android.sdk.internal.database.model.EditionOfEventFields -import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.util.database.RealmMigrator internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) { @@ -27,11 +27,9 @@ internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 4 override fun doMigrate(realm: DynamicRealm) { // content(string) & senderId(string) have been removed and replaced by a link to the actual event realm.schema.get("EditionOfEvent") - ?.removeField("senderId") - ?.removeField("content") ?.addRealmObjectField(EditionOfEventFields.EVENT.`$`, realm.schema.get("EventEntity")!!) ?.transform { dynamicObject -> - realm.where(EventEntity::javaClass.name) + realm.where("EventEntity") .equalTo(EventEntityFields.EVENT_ID, dynamicObject.getString(EditionOfEventFields.EVENT_ID)) .equalTo(EventEntityFields.SENDER, dynamicObject.getString("senderId")) .findFirst() @@ -39,5 +37,7 @@ internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 4 dynamicObject.setObject(EditionOfEventFields.EVENT.`$`, it) } } + ?.removeField("senderId") + ?.removeField("content") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt index 63409a15bb..91e709e464 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/EventExt.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.events import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.ValidDecryptedEvent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent @@ -34,32 +33,3 @@ internal fun Event.getFixedRoomMemberContent(): RoomMemberContent? { content } } - -fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { - if (!this.isEncrypted()) return null - val decryptedContent = this.getDecryptedContent() ?: return null - val eventId = this.eventId ?: return null - val roomId = this.roomId ?: return null - val type = this.getDecryptedType() ?: return null - val senderKey = this.getSenderKey() ?: return null - val algorithm = this.content?.get("algorithm") as? String ?: return null - - // copy the relation as it's in clear in the encrypted content - val updatedContent = this.content.get("m.relates_to")?.let { - decryptedContent.toMutableMap().apply { - put("m.relates_to", it) - } - } ?: decryptedContent - return ValidDecryptedEvent( - type = type, - eventId = eventId, - clearContent = updatedContent, - prevContent = this.prevContent, - originServerTs = this.originServerTs ?: 0, - cryptoSenderKey = senderKey, - roomId = roomId, - unsignedData = this.unsignedData, - redacts = this.redacts, - algorithm = algorithm - ) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt index ea14aacfe4..41d0c3f6ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -21,9 +21,9 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt index 12ff9c1d37..861f343179 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt index 8dead42f60..1d1933df84 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,8 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent -import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent class ValidDecryptedEventTest { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 3c8b342a1a..57a4388f74 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.events.model.getMsgType import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -44,7 +45,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited -import org.matrix.android.sdk.internal.session.events.toValidDecryptedEvent import javax.inject.Inject /** From bec8b5f71ecd6d8c2ab2771c6fc7926af82c5acb Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 24 Nov 2022 12:45:11 +0100 Subject: [PATCH 305/679] code review --- ... EditAggregatedSummaryEntityMapperTest.kt} | 52 ++++++++++++++++--- .../session/event/ValidDecryptedEventTest.kt | 2 +- 2 files changed, 45 insertions(+), 9 deletions(-) rename matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/{EditAggregationSummaryMapperTest.kt => EditAggregatedSummaryEntityMapperTest.kt} (57%) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt similarity index 57% rename from matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt index 861f343179..7ad5bb40e3 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregationSummaryMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt @@ -19,17 +19,17 @@ package org.matrix.android.sdk.internal.database.mapper import io.mockk.every import io.mockk.mockk import io.realm.RealmList -import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldNotBe import org.junit.Test import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventEntity -class EditAggregationSummaryMapperTest { +class EditAggregatedSummaryEntityMapperTest { @Test - fun test() { + fun `test mapping summary entity to model`() { val edits = RealmList( EditionOfEvent( timestamp = 0L, @@ -56,12 +56,48 @@ class EditAggregationSummaryMapperTest { val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) mapped shouldNotBe null - mapped!!.sourceEvents.size shouldBe 2 - mapped.localEchos.size shouldBe 1 - mapped.localEchos.first() shouldBe "e2" + mapped!!.sourceEvents.size shouldBeEqualTo 2 + mapped.localEchos.size shouldBeEqualTo 1 + mapped.localEchos.first() shouldBeEqualTo "e2" - mapped.lastEditTs shouldBe 30L - mapped.latestEdit?.eventId shouldBe "e2" + mapped.lastEditTs shouldBeEqualTo 30L + mapped.latestEdit?.eventId shouldBeEqualTo "e2" + } + + @Test + fun `event with lexicographically largest event_id is treated as more recent`() { + val lowerId = "\$Albatross" + val higherId = "\$Zebra" + + (higherId > lowerId) shouldBeEqualTo true + val timestamp = 1669288766745L + val edits = RealmList( + EditionOfEvent( + timestamp = timestamp, + eventId = lowerId, + isLocalEcho = false, + event = mockEvent(lowerId) + ), + EditionOfEvent( + timestamp = timestamp, + eventId = higherId, + isLocalEcho = false, + event = mockEvent(higherId) + ), + EditionOfEvent( + timestamp = 1L, + eventId = "e2", + isLocalEcho = true, + event = mockEvent("e2") + ) + ) + + val fakeSummaryEntity = mockk { + every { editions } returns edits + } + val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) + mapped!!.lastEditTs shouldBeEqualTo timestamp + mapped.latestEdit?.eventId shouldBeEqualTo higherId } private fun mockEvent(eventId: String): EventEntity { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt index 1d1933df84..5fda242b90 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt @@ -31,7 +31,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent class ValidDecryptedEventTest { - val fakeEvent = Event( + private val fakeEvent = Event( type = EventType.ENCRYPTED, eventId = "\$eventId", roomId = "!fakeRoom", From 59ac3b4f8b6c06d9a0bbd972bb257266dc78d199 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 24 Nov 2022 15:26:59 +0300 Subject: [PATCH 306/679] Update new strings of unverified sessions alert. --- library/ui-strings/src/main/res/values/strings.xml | 4 ++++ .../java/im/vector/app/features/home/HomeDetailFragment.kt | 4 ++-- .../java/im/vector/app/features/home/NewHomeDetailFragment.kt | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f1d5bfbcad..3945a80393 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2647,8 +2647,12 @@ Unencrypted Encrypted by an unverified device The authenticity of this encrypted message can\'t be guaranteed on this device. + Review where you’re logged in + Verify all your sessions to ensure your account & messages are safe + You have unverified sessions + Review to ensure your account is safe Verify the new login accessing your account: %1$s diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index e824dc1820..7552b934e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -256,8 +256,8 @@ class HomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 5956646eab..62d7e58bdb 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -270,8 +270,8 @@ class NewHomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) From 9ca7415f5a8f143bc397c0e4ebfa109274ebd57b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 14 Nov 2022 15:02:01 +0100 Subject: [PATCH 307/679] Remove usage of Buildkite. Build number is just removed. Related script will need to be updated separately. --- README.md | 6 +++--- tools/gradle/doctor.gradle | 2 +- tools/install/installFromBuildkite.sh | 3 +++ tools/release/download_buildkite_artifacts.py | 4 ++++ tools/release/releaseScript.sh | 2 +- vector-app/build.gradle | 14 ++------------ vector-app/signature/README.md | 4 ---- .../main/java/im/vector/app/VectorApplication.kt | 2 +- .../java/im/vector/app/core/di/SingletonModule.kt | 1 - vector/src/main/AndroidManifest.xml | 4 ++-- .../im/vector/app/core/resources/BuildMeta.kt | 1 - .../app/features/login/LoginSplashFragment.kt | 3 +-- .../ftueauth/FtueAuthSplashCarouselFragment.kt | 2 +- .../onboarding/ftueauth/FtueAuthSplashFragment.kt | 3 +-- .../vector/app/features/rageshake/BugReporter.kt | 7 +------ .../rageshake/VectorUncaughtExceptionHandler.kt | 2 +- .../settings/VectorSettingsHelpAboutFragment.kt | 2 +- .../app/features/version/VersionProvider.kt | 15 +++------------ 18 files changed, 26 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index e351b64927..e8fceb2eb2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) +[![Latest build](https://github.com/vector-im/element-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Adevelop) [![Weblate](https://translate.element.io/widgets/element-android/-/svg-badge.svg)](https://translate.element.io/engage/element-android/?utm_source=widget) [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vector-im_element-android) @@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.app) [Get it on F-Droid](https://f-droid.org/app/im.vector.app) -Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nightly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml) +Build of develop branch: [![GitHub Action](https://github.com/vector-im/element-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Adevelop) Nightly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml) # New Android SDK @@ -40,7 +40,7 @@ If you would like to receive releases more quickly (bearing in mind that they ma 1. [Sign up to receive beta releases](https://play.google.com/apps/testing/im.vector.app) via the Google Play Store. 2. Install a [release APK](https://github.com/vector-im/element-android/releases) directly - download the relevant .apk file and allow installing from untrusted sources in your device settings. Note: these releases are the Google Play version, which depend on some Google services. If you prefer to avoid that, try the latest dev builds, and choose the F-Droid version. -3. If you're really brave, install the [very latest dev build](https://buildkite.com/matrix-dot-org/element-android/builds/latest?branch=develop&state=passed) - click on *Assemble (GPlay or FDroid) Debug version* then on *Artifacts*. +3. If you're really brave, install the [very latest dev build](https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Adevelop) - pick a build, then click on `Summary` to download the APKs from there: `vector-Fdroid-debug` and `vector-Gplay-debug` contains the APK for the desired store. Each file contains 5 APKs. 4 APKs for every supported specific architecture of device. In doubt you can install the `universal` APK. ## Contributing diff --git a/tools/gradle/doctor.gradle b/tools/gradle/doctor.gradle index 7a7adad062..edd3069402 100644 --- a/tools/gradle/doctor.gradle +++ b/tools/gradle/doctor.gradle @@ -1,6 +1,6 @@ // Default configuration copied from https://runningcode.github.io/gradle-doctor/configuration/ -def isCiBuild = System.env.BUILDKITE == "true" || System.env.GITHUB_ACTIONS == "true" +def isCiBuild = System.env.GITHUB_ACTIONS == "true" println "Is CI build: $isCiBuild" doctor { diff --git a/tools/install/installFromBuildkite.sh b/tools/install/installFromBuildkite.sh index e47902e31b..759a2a0035 100755 --- a/tools/install/installFromBuildkite.sh +++ b/tools/install/installFromBuildkite.sh @@ -3,6 +3,9 @@ # Exit on any error set -e +echo "Sorry, this script needs to be updated to download APKs from GitHub action. Buildkite is not building APKs anymore." +exit 1 + if [[ "$#" -ne 1 ]]; then echo "Usage: $0 BUILDKITE_TOKEN" >&2 exit 1 diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py index 7a824be806..00f93a52e5 100755 --- a/tools/release/download_buildkite_artifacts.py +++ b/tools/release/download_buildkite_artifacts.py @@ -31,6 +31,10 @@ ### Arguments +print("Sorry, this script needs to be updated to download APKs from GitHub action. Buildkite is not building APKs anymore.") +exit(1) + + parser = argparse.ArgumentParser(description='Download artifacts from Buildkite.') parser.add_argument('-t', '--token', diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index 685093d65e..943e9771f2 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -214,7 +214,7 @@ else fi printf "\n================================================================================\n" -read -p "Wait for Buildkite https://buildkite.com/matrix-dot-org/element-android/builds?branch=main to build the 'main' branch. Press enter when it's done." +read -p "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch. Press enter when it's done." printf "\n================================================================================\n" printf "Running the release script...\n" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index a9b16f8c6c..86b94a8497 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -76,15 +76,8 @@ static def gitRevisionDate() { } static def gitBranchName() { - def fromEnv = System.env.BUILDKITE_BRANCH as String ?: "" - - if (!fromEnv.isEmpty()) { - return fromEnv - } else { - // Note: this command return "HEAD" on Buildkite, so use the system env 'BUILDKITE_BRANCH' content first - def cmd = "git rev-parse --abbrev-ref HEAD" - return cmd.execute().text.trim() - } + def cmd = "git rev-parse --abbrev-ref HEAD" + return cmd.execute().text.trim() } // For Google Play build, build on any other branch than main will have a "-dev" suffix @@ -122,8 +115,6 @@ project.android.buildTypes.all { buildType -> // 64 bits have greater value than 32 bits ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].withDefault { 0 } -def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 - android { namespace "im.vector.application" // Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use @@ -155,7 +146,6 @@ android { buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_REVISION_DATE", "\"${gitRevisionDate()}\"" buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"" - buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/vector-app/signature/README.md b/vector-app/signature/README.md index 7d9005f1f4..34d40b45bd 100644 --- a/vector-app/signature/README.md +++ b/vector-app/signature/README.md @@ -1,10 +1,6 @@ ## Debug signature -Buildkite CI tool uses docker images to build the Android application, and it looks like the debug signature is changed at each build. - -So it's not possible for user to upgrade the application with the last build from buildkite without uninstalling the application. - This folder contains a debug signature, and the debug build will uses this signature to build the APK. The validity of the signature is 30 years. So it has to be replaced before June 2049 :). diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index ec0a6cb2a4..b89529715c 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -239,7 +239,7 @@ class VectorApplication : } private fun logInfo() { - val appVersion = versionProvider.getVersion(longFormat = true, useBuildNumber = true) + val appVersion = versionProvider.getVersion(longFormat = true) val sdkVersion = Matrix.getSdkVersion() val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date()) diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index 3c1cea57ec..28ca761ace 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -221,7 +221,6 @@ import javax.inject.Singleton gitRevision = BuildConfig.GIT_REVISION, gitRevisionDate = BuildConfig.GIT_REVISION_DATE, gitBranchName = BuildConfig.GIT_BRANCH_NAME, - buildNumber = BuildConfig.BUILD_NUMBER, flavorDescription = BuildConfig.FLAVOR_DESCRIPTION, flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION, ) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index d28312ac1c..a26be23456 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -117,7 +117,7 @@ android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" /> - + - + (VectorPreferences.SETTINGS_VERSION_PREFERENCE_KEY)!!.let { it.summary = buildString { - append(versionProvider.getVersion(longFormat = false, useBuildNumber = true)) + append(versionProvider.getVersion(longFormat = false)) if (buildMeta.isDebug) { append(" ") append(buildMeta.gitBranchName) diff --git a/vector/src/main/java/im/vector/app/features/version/VersionProvider.kt b/vector/src/main/java/im/vector/app/features/version/VersionProvider.kt index 4c8188dc8b..2b7406813f 100644 --- a/vector/src/main/java/im/vector/app/features/version/VersionProvider.kt +++ b/vector/src/main/java/im/vector/app/features/version/VersionProvider.kt @@ -25,7 +25,7 @@ class VersionProvider @Inject constructor( private val buildMeta: BuildMeta, ) { - fun getVersion(longFormat: Boolean, useBuildNumber: Boolean): String { + fun getVersion(longFormat: Boolean): String { var result = "${buildMeta.versionName} [${versionCodeProvider.getVersionCode()}]" var flavor = buildMeta.flavorShortDescription @@ -34,19 +34,10 @@ class VersionProvider @Inject constructor( flavor += "-" } - var gitVersion = buildMeta.gitRevision + val gitVersion = buildMeta.gitRevision val gitRevisionDate = buildMeta.gitRevisionDate - val buildNumber = buildMeta.buildNumber - var useLongFormat = longFormat - - if (useBuildNumber && buildNumber != "0") { - // It's a build from CI - gitVersion = "b$buildNumber" - useLongFormat = false - } - - result += if (useLongFormat) { + result += if (longFormat) { " ($flavor$gitVersion-$gitRevisionDate)" } else { " ($flavor$gitVersion)" From cccfad03cebc8b4cac7af266546159cceafa7f46 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 14 Nov 2022 15:28:04 +0100 Subject: [PATCH 308/679] changelog --- changelog.d/7583.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7583.misc diff --git a/changelog.d/7583.misc b/changelog.d/7583.misc new file mode 100644 index 0000000000..3c63aeaadf --- /dev/null +++ b/changelog.d/7583.misc @@ -0,0 +1 @@ +Remove usage of Buildkite. From 8795ddb3c2654bd14059bdb8690de598e23d6d68 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 16 Nov 2022 14:51:45 +0100 Subject: [PATCH 309/679] Add git commit sha to the version details (splash screen and preference screen) --- .../java/im/vector/app/features/login/LoginSplashFragment.kt | 2 +- .../onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt | 2 +- .../app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt | 2 +- .../app/features/settings/VectorSettingsHelpAboutFragment.kt | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt index cf89475617..dd563483ec 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt @@ -65,7 +65,7 @@ class LoginSplashFragment : views.loginSplashVersion.isVisible = true @SuppressLint("SetTextI18n") views.loginSplashVersion.text = "Version : ${buildMeta.versionName}\n" + - "Branch: ${buildMeta.gitBranchName}" + "Branch: ${buildMeta.gitBranchName} ${buildMeta.gitRevision}" views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index bbb52a2bf5..3d23d2b4c3 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -95,7 +95,7 @@ class FtueAuthSplashCarouselFragment : views.loginSplashVersion.isVisible = true @SuppressLint("SetTextI18n") views.loginSplashVersion.text = "Version : ${buildMeta.versionName}\n" + - "Branch: ${buildMeta.gitBranchName}" + "Branch: ${buildMeta.gitBranchName} ${buildMeta.gitRevision}" views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) } } views.splashCarousel.registerAutomaticUntilInteractionTransitions() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt index 71c7b1f1f6..3c8f3c25d9 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt @@ -67,7 +67,7 @@ class FtueAuthSplashFragment : views.loginSplashVersion.isVisible = true @SuppressLint("SetTextI18n") views.loginSplashVersion.text = "Version : ${buildMeta.versionName}\n" + - "Branch: ${buildMeta.gitBranchName}" + "Branch: ${buildMeta.gitBranchName} ${buildMeta.gitRevision}" views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt index aaf6dd208c..f1a9b724e2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt @@ -73,6 +73,8 @@ class VectorSettingsHelpAboutFragment : if (buildMeta.isDebug) { append(" ") append(buildMeta.gitBranchName) + append(" ") + append(buildMeta.gitRevision) } } From 83a10c37a64c90ea40d958a3eff8645e98982755 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 16 Nov 2022 17:31:35 +0100 Subject: [PATCH 310/679] Install app from GitHub action --- tools/install/installFromGitHub.sh | 94 +++++++++++++ tools/release/download_github_artifacts.py | 154 +++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100755 tools/install/installFromGitHub.sh create mode 100755 tools/release/download_github_artifacts.py diff --git a/tools/install/installFromGitHub.sh b/tools/install/installFromGitHub.sh new file mode 100755 index 0000000000..c6af9c376d --- /dev/null +++ b/tools/install/installFromGitHub.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# Exit on any error +set -e + +if [[ "$#" -ne 1 ]]; then + echo "Usage: $0 GitHub_token" >&2 + exit 1 +fi + +gitHubToken=$1 + +# Path where the app is cloned (it's where this project has been cloned) +appPath=$(dirname $(dirname $(dirname $0))) +# Path where the APK will be downloaded from CI (it's a dir) +baseImportPath="${appPath}/tmp/DebugApks" + +# Select device +serialNumber=$(${appPath}/tools/install/androidSelectDevice.sh) + +# Detect device architecture +arch=$(adb -s ${serialNumber} shell getprop ro.product.cpu.abi) + +echo +echo "Will install the application on device ${serialNumber} with arch ${arch}" + +# Artifact URL +echo +read -p "Artifact url (ex: https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942121)? " artifactUrl + +## Example of default value for Gplay +#artifactUrl=${artifactUrl:-https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942121} +## Example of default value for FDroid +# artifactUrl=${artifactUrl:-https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942119} + +artifactId=$(echo ${artifactUrl} | rev | cut -d'/' -f1 | rev) + +# Download files +targetPath=${baseImportPath}/${artifactId} + +filename="artifact.zip" + +fullFilePath="${targetPath}/${filename}" + +# Check if file already exists +if test -f "$fullFilePath"; then + read -p "$fullFilePath already exists. Override (yes/no) default to no ? " download + download=${download:-no} +else + download="yes" +fi + +# Ignore error from now +set +e + +if [ ${download} == "yes" ]; then + echo "Downloading ${filename} to ${targetPath}..." + python3 ${appPath}/tools/release/download_github_artifacts.py \ + --token ${gitHubToken} \ + --artifactUrl ${artifactUrl} \ + --directory ${targetPath} \ + --filename ${filename} \ + --ignoreErrors +fi + +echo "Unzipping ${filename}..." +unzip $fullFilePath -d ${targetPath} + +## gplay or fdroid +if test -d "${targetPath}/gplay"; then + variant="gplay" +elif test -d "${targetPath}/fdroid"; then + variant="fdroid" +else + echo "No variant found" + exit 1 +fi + +fullApkPath="${targetPath}/${variant}/debug/vector-${variant}-${arch}-debug.apk" + +echo "Installing ${fullApkPath} to device ${serialNumber}..." +adb -s ${serialNumber} install -r ${fullApkPath} + +# Check error and propose to uninstall and retry installing +if [[ "$?" -ne 0 ]]; then + read -p "Error, do you want to uninstall the application then retry (yes/no) default to no ? " retry + retry=${retry:-no} + if [ ${retry} == "yes" ]; then + echo "Uninstalling..." + adb -s ${serialNumber} uninstall im.vector.app.debug + echo "Installing again..." + adb -s ${serialNumber} install -r ${fullApkPath} + fi +fi diff --git a/tools/release/download_github_artifacts.py b/tools/release/download_github_artifacts.py new file mode 100755 index 0000000000..892a4affa6 --- /dev/null +++ b/tools/release/download_github_artifacts.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# +# Copyright 2022 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import argparse +import hashlib +import json +import os +# Run `pip3 install requests` if not installed yet +import requests + +# This script downloads artifacts from GitHub. +# Ref: https://docs.github.com/en/rest/actions/artifacts#get-an-artifact + +error = False + +### Arguments + +parser = argparse.ArgumentParser(description='Download artifacts from GitHub.') +parser.add_argument('-t', + '--token', + required=True, + help='The GitHub token with read access.') +parser.add_argument('-a', + '--artifactUrl', + required=True, + help='the artifact_url from GitHub.') +parser.add_argument('-f', + '--filename', + help='the filename, if not provided, will use the artifact name.') +parser.add_argument('-i', + '--ignoreErrors', + help='Ignore errors that can be ignored. Build state and number of artifacts.', + action="store_true") +parser.add_argument('-d', + '--directory', + default="", + help='the target directory, where files will be downloaded. If not provided the build number will be used to create a directory.') +parser.add_argument('-v', + '--verbose', + help="increase output verbosity.", + action="store_true") +parser.add_argument('-s', + '--simulate', + help="simulate action, do not create folder or download any file.", + action="store_true") + +args = parser.parse_args() + +if args.verbose: + print("Argument:") + print(args) + +# Split the artifact URL to get information +# Ex: https://github.com/vector-im/element-android/suites/9293388174/artifacts/435942121 +artifactUrl = args.artifactUrl +if not artifactUrl.startswith('https://github.com/'): + print("❌ Invalid parameter --artifactUrl %s. Must start with 'https://github.com/'" % artifactUrl) + exit(1) +if "/artifacts/" not in artifactUrl: + print("❌ Invalid parameter --artifactUrl %s. Must contain '/artifacts/'" % artifactUrl) + exit(1) +artifactItems = artifactUrl.split("/") +if len(artifactItems) != 9: + print("❌ Invalid parameter --artifactUrl %s. Please check the format." % (artifactUrl)) + exit(1) + +gitHubRepoOwner = artifactItems[3] +gitHubRepo = artifactItems[4] +artifactId = artifactItems[8] + +if args.verbose: + print("gitHubRepoOwner: %s, gitHubRepo: %s, artifactId: %s" % (gitHubRepoOwner, gitHubRepo, artifactId)) + +headers = { + 'Authorization': "Bearer %s" % args.token, + 'Accept': 'application/vnd.github+json' +} +base_url = "https://api.github.com/repos/%s/%s/actions/artifacts/%s" % (gitHubRepoOwner, gitHubRepo, artifactId) + +### Fetch build state + +print("Getting artifacts data of project '%s/%s' artifactId '%s'..." % (gitHubRepoOwner, gitHubRepo, artifactId)) + +if args.verbose: + print("Url: %s" % base_url) + +r = requests.get(base_url, headers=headers) +data = json.loads(r.content.decode()) + +if args.verbose: + print("Json data:") + print(data) + +if args.verbose: + print("Create subfolder %s to download artifacts..." % artifactId) + +if args.directory == "": + targetDir = artifactId +else: + targetDir = args.directory + +if not args.simulate: + os.makedirs(targetDir, exist_ok=True) + +url = data.get("archive_download_url") +if args.filename is not None: + filename = args.filename +else: + filename = data.get("name") + ".zip" + +## Print some info about the artifact origin +commitLink = "https://github.com/%s/%s/commit/%s" % (gitHubRepoOwner, gitHubRepo, data.get("workflow_run").get("head_sha")) +print("Preparing to download artifact `%s`, built from branch: `%s` (commit %s)" % (data.get("name"), data.get("workflow_run").get("head_branch"), commitLink)) + +if args.verbose: + print() + print("Artifact url: %s" % url) + +target = targetDir + "/" + filename +sizeInBytes = data.get("size_in_bytes") +print("Downloading %s to '%s' (file size is %s bytes, this may take a while)..." % (filename, targetDir, sizeInBytes)) +if not args.simulate: + # open file to write in binary mode + with open(target, "wb") as file: + # get request + response = requests.get(url, headers=headers) + # write to file + file.write(response.content) + print("Verifying file size...") + # get the file size + size = os.path.getsize(target) + if sizeInBytes != size: + # error = True + print("Warning, file size mismatch: expecting %s and get %s. This is just a warning for now..." % (sizeInBytes, size)) + +if error: + print("❌ Error(s) occurred, please check the log") + exit(1) +else: + print("Done!") From 451df7558dfcaaa502cc0a7655f11e0ec66da952 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 16 Nov 2022 17:41:41 +0100 Subject: [PATCH 311/679] Buildkite scripts can still be used. --- tools/install/installFromBuildkite.sh | 3 --- tools/release/download_buildkite_artifacts.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/tools/install/installFromBuildkite.sh b/tools/install/installFromBuildkite.sh index 759a2a0035..e47902e31b 100755 --- a/tools/install/installFromBuildkite.sh +++ b/tools/install/installFromBuildkite.sh @@ -3,9 +3,6 @@ # Exit on any error set -e -echo "Sorry, this script needs to be updated to download APKs from GitHub action. Buildkite is not building APKs anymore." -exit 1 - if [[ "$#" -ne 1 ]]; then echo "Usage: $0 BUILDKITE_TOKEN" >&2 exit 1 diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py index 00f93a52e5..7a824be806 100755 --- a/tools/release/download_buildkite_artifacts.py +++ b/tools/release/download_buildkite_artifacts.py @@ -31,10 +31,6 @@ ### Arguments -print("Sorry, this script needs to be updated to download APKs from GitHub action. Buildkite is not building APKs anymore.") -exit(1) - - parser = argparse.ArgumentParser(description='Download artifacts from Buildkite.') parser.add_argument('-t', '--token', From 211c0c2dc52e3988b9aa8154f5019fefbd49c263 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Nov 2022 15:36:13 +0100 Subject: [PATCH 312/679] Add doc for script installFromGitHub.sh --- docs/installing_from_ci.md | 51 ++++++++++++++++++++++++++++++ tools/install/installFromGitHub.sh | 3 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 docs/installing_from_ci.md diff --git a/docs/installing_from_ci.md b/docs/installing_from_ci.md new file mode 100644 index 0000000000..42f4b56e28 --- /dev/null +++ b/docs/installing_from_ci.md @@ -0,0 +1,51 @@ +## Installing from CI + + + + * [Installing from Buildkite](#installing-from-buildkite) + * [Installing from GitHub](#installing-from-github) + * [Create a GitHub token](#create-a-github-token) + * [Provide artifact URL](#provide-artifact-url) + * [Next steps](#next-steps) + + + +Installing APK build by the CI is possible + +### Installing from Buildkite + +The script `./tools/install/installFromBuildkite.sh` can be used, but Builkite will be removed soon. See next section. + +### Installing from GitHub + +To install an APK built by a GitHub action, run the script `./tools/install/installFromGitHub.sh`. You will need to pass a GitHub token to do so. + +#### Create a GitHub token + +You can create a GitHub token going to your Github account, at this page: [https://github.com/settings/tokens](https://github.com/settings/tokens). + +You need to create a token (classic) with the scope `repo/public_repo`. So just check the corresponding checkbox. +Validity can be long since the scope of this token is limited. You will still be able to delete the token and generate a new one. +Click on Generate token and save the token locally. + +### Provide artifact URL + +The script will ask for an artifact URL. You can get this artifact URL by following these steps: + +- open the pull request +- in the check at the bottom, click on `APK Build / Build debug APKs` +- click on `Summary` +- scroll to the bottom of the page +- copy the link `vector-Fdroid-debug` if you want the F-Droid variant or `vector-Gplay-debug` if you want the Gplay variant. + +The copied link can be provided to the script. + +### Next steps + +The script will download the artifact, unzip it and install the correct version (regarding arch) on your device. + +Files will be added to the folder `./tmp/DebugApks`. Feel free to cleanup this folder from time to time, the script will not delete files. + +### Future improvement + +The script could ask the user for a Pull Request number and Gplay/Fdroid choice like it was done with Buildkite script. Using GitHub API may be possible to do that. diff --git a/tools/install/installFromGitHub.sh b/tools/install/installFromGitHub.sh index c6af9c376d..6928003773 100755 --- a/tools/install/installFromGitHub.sh +++ b/tools/install/installFromGitHub.sh @@ -4,7 +4,8 @@ set -e if [[ "$#" -ne 1 ]]; then - echo "Usage: $0 GitHub_token" >&2 + echo "Usage: $0 GitHub_Token" >&2 + echo "Read more about this script in the doc ./docs/installing_from_ci.md" exit 1 fi From deb4730d40017e28d2ff891ee045e5f0686c47b1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 24 Nov 2022 16:43:37 +0100 Subject: [PATCH 313/679] Add section about knit tool --- CONTRIBUTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e3c784dac..62762d7231 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -126,6 +126,23 @@ Note that you can run For ktlint to fix some detected errors for you (you still have to check and commit the fix of course) +#### knit + +[knit](https://github.com/Kotlin/kotlinx-knit) is a tool which checks markdown files on the project. Also it generates/updates the table of content (toc) of the markdown files. + +So everytime the toc should be updated, just run +

+./gradlew knit
+
+ +and commit the changes. + +The CI will check that markdown files are up to date by running + +
+./gradlew knitCheck
+
+ #### lint

From fa3b440a22d4c2cd618c8c05b45c40171dcac216 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 24 Nov 2022 16:44:07 +0100
Subject: [PATCH 314/679] Run knit.

---
 CONTRIBUTING.md            | 1 +
 docs/installing_from_ci.md | 1 +
 2 files changed, 2 insertions(+)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 62762d7231..40ae848415 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,6 +13,7 @@
   * [Code quality](#code-quality)
     * [Internal tool](#internal-tool)
     * [ktlint](#ktlint)
+    * [knit](#knit)
     * [lint](#lint)
   * [Unit tests](#unit-tests)
   * [Tests](#tests)
diff --git a/docs/installing_from_ci.md b/docs/installing_from_ci.md
index 42f4b56e28..01fb4afef2 100644
--- a/docs/installing_from_ci.md
+++ b/docs/installing_from_ci.md
@@ -7,6 +7,7 @@
     * [Create a GitHub token](#create-a-github-token)
   * [Provide artifact URL](#provide-artifact-url)
   * [Next steps](#next-steps)
+  * [Future improvement](#future-improvement)
 
 
 

From 492e8424106ecb2f895dea906f050f0c64467744 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 24 Nov 2022 18:05:24 +0100
Subject: [PATCH 315/679] Fix the fixture.

---
 .../test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/vector/src/test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt
index 4f4649106d..e4dee0e474 100644
--- a/vector/src/test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt
+++ b/vector/src/test/java/im/vector/app/test/fixtures/BuildMetaFixture.kt
@@ -26,7 +26,6 @@ fun aBuildMeta() = BuildMeta(
         gitRevision = "abcdef",
         gitRevisionDate = "01-01-01",
         gitBranchName = "a-branch-name",
-        buildNumber = "100",
         flavorDescription = "Gplay",
         flavorShortDescription = "",
 )

From 18bcc83a4603bb15c750e5de3721ce80e585de62 Mon Sep 17 00:00:00 2001
From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com>
Date: Fri, 25 Nov 2022 09:49:06 +0100
Subject: [PATCH 316/679] added read receipts for threads (#7474)

---
 changelog.d/6996.sdk                          |  1 +
 .../org/matrix/android/sdk/flow/FlowRoom.kt   |  4 +-
 .../sdk/api/session/room/model/ReadReceipt.kt |  3 +-
 .../sdk/api/session/room/read/ReadService.kt  | 14 +++--
 .../database/helper/ChunkEntityHelper.kt      |  2 +-
 .../database/helper/ThreadEventsHelper.kt     | 60 +++++++++----------
 .../mapper/ReadReceiptsSummaryMapper.kt       |  2 +-
 .../database/model/ReadReceiptEntity.kt       |  1 +
 .../database/model/TimelineEventEntity.kt     |  5 ++
 .../internal/database/query/ReadQueries.kt    | 26 +++++---
 .../query/ReadReceiptEntityQueries.kt         | 38 +++++++++---
 .../sdk/internal/session/room/RoomAPI.kt      |  3 +-
 .../session/room/read/DefaultReadService.kt   | 34 ++++++++---
 .../internal/session/room/read/ReadBody.kt    | 25 ++++++++
 .../session/room/read/SetReadMarkersTask.kt   | 22 +++++--
 .../room/summary/RoomSummaryUpdater.kt        |  8 ++-
 .../session/room/timeline/DefaultTimeline.kt  |  2 +-
 .../sync/handler/room/ReadReceiptHandler.kt   | 26 +++++---
 .../home/room/detail/TimelineViewModel.kt     |  7 ++-
 .../timeline/TimelineEventController.kt       | 13 +++-
 .../factory/ReadReceiptsItemFactory.kt        | 11 +++-
 .../NotificationBroadcastReceiver.kt          |  2 +-
 22 files changed, 215 insertions(+), 94 deletions(-)
 create mode 100644 changelog.d/6996.sdk
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt

diff --git a/changelog.d/6996.sdk b/changelog.d/6996.sdk
new file mode 100644
index 0000000000..588ec160d7
--- /dev/null
+++ b/changelog.d/6996.sdk
@@ -0,0 +1 @@
+Added support for read receipts in threads. Now user in a room can have multiple read receipts (one per thread + one in main thread + one without threadId)
diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
index a6b4cc98a6..7ad342b22f 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt
@@ -100,8 +100,8 @@ class FlowRoom(private val room: Room) {
         return room.readService().getReadMarkerLive().asFlow()
     }
 
-    fun liveReadReceipt(): Flow> {
-        return room.readService().getMyReadReceiptLive().asFlow()
+    fun liveReadReceipt(threadId: String?): Flow> {
+        return room.readService().getMyReadReceiptLive(threadId).asFlow()
     }
 
     fun liveEventReadReceipts(eventId: String): Flow> {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
index 5639730219..da7e4ea928 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
@@ -18,5 +18,6 @@ package org.matrix.android.sdk.api.session.room.model
 
 data class ReadReceipt(
         val roomMember: RoomMemberSummary,
-        val originServerTs: Long
+        val originServerTs: Long,
+        val threadId: String?
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
index dac1a1a773..83680ec2d8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
@@ -34,12 +34,14 @@ interface ReadService {
     /**
      * Force the read marker to be set on the latest event.
      */
-    suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH)
+    suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, mainTimeLineOnly: Boolean = true)
 
     /**
      * Set the read receipt on the event with provided eventId.
+     * @param eventId the id of the event where read receipt will be set
+     * @param threadId the id of the thread in which read receipt will be set. For main thread use [ReadService.THREAD_ID_MAIN] constant
      */
-    suspend fun setReadReceipt(eventId: String)
+    suspend fun setReadReceipt(eventId: String, threadId: String)
 
     /**
      * Set the read marker on the event with provided eventId.
@@ -59,10 +61,10 @@ interface ReadService {
     /**
      * Returns a live read receipt id for the room.
      */
-    fun getMyReadReceiptLive(): LiveData>
+    fun getMyReadReceiptLive(threadId: String?): LiveData>
 
     /**
-     * Get the eventId where the read receipt for the provided user is.
+     * Get the eventId from the main timeline where the read receipt for the provided user is.
      * @param userId the id of the user to look for
      *
      * @return the eventId where the read receipt for the provided user is attached, or null if not found
@@ -74,4 +76,8 @@ interface ReadService {
      * @param eventId the event
      */
     fun getEventReadReceiptsLive(eventId: String): LiveData>
+
+    companion object {
+        const val THREAD_ID_MAIN = "main"
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 149a2eebfe..43f84e771a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -132,7 +132,7 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
     val originServerTs = eventEntity.originServerTs
     if (originServerTs != null) {
         val timestampOfEvent = originServerTs.toDouble()
-        val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
+        val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId)
         // If the synced RR is older, update
         if (timestampOfEvent > readReceiptOfSender.originServerTs) {
             val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index dfac7f6708..7999a2ea14 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -65,11 +65,11 @@ internal fun Map.updateThreadSummaryIfNeeded(
                     inThreadMessages = inThreadMessages,
                     latestMessageTimelineEventEntity = latestEventInThread
             )
-        }
-    }
 
-    if (shouldUpdateNotifications) {
-        updateNotificationsNew(roomId, realm, currentUserId)
+            if (shouldUpdateNotifications) {
+                updateThreadNotifications(roomId, realm, currentUserId, rootThreadEventId)
+            }
+        }
     }
 }
 
@@ -273,8 +273,8 @@ internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm,
 /**
  * Find the read receipt for the current user.
  */
-internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
-        ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
+internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): String? =
+        ReadReceiptEntity.where(realm, roomId = roomId, userId = userId, threadId = threadId)
                 .findFirst()
                 ?.eventId
 
@@ -293,28 +293,29 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin
  * Important: It will work only with the latest chunk, while read marker will be changed
  * immediately so we should not display wrong notifications
  */
-internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
-    val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
+internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUserId: String, rootThreadEventId: String) {
+    val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return
 
     val readReceiptChunk = ChunkEntity
             .findIncludingEvent(realm, readReceipt) ?: return
 
-    val readReceiptChunkTimelineEvents = readReceiptChunk
+    val readReceiptChunkThreadEvents = readReceiptChunk
             .timelineEvents
             .where()
             .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
+            .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
             .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
             .findAll() ?: return
 
-    val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
+    val readReceiptChunkPosition = readReceiptChunkThreadEvents.indexOfFirst { it.eventId == readReceipt }
 
     if (readReceiptChunkPosition == -1) return
 
-    if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
+    if (readReceiptChunkPosition < readReceiptChunkThreadEvents.lastIndex) {
         // If the read receipt is found inside the chunk
 
-        val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
-                .slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
+        val threadEventsAfterReadReceipt = readReceiptChunkThreadEvents
+                .slice(readReceiptChunkPosition..readReceiptChunkThreadEvents.lastIndex)
                 .filter { it.root?.isThread() == true }
 
         // In order for the below code to work for old events, we should save the previous read receipt
@@ -343,26 +344,21 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId:
                     it.root?.rootThreadEventId
                 }
 
-        // Find the root events in the new thread events
-        val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
-
-        // Update root thread events only if the user have participated in
-        rootThreads.forEach { eventId ->
-            val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
-                    realm = realm,
-                    roomId = roomId,
-                    rootThreadEventId = eventId,
-                    senderId = currentUserId
-            )
-            val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
-
-            if (isUserParticipating) {
-                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
-            }
+        // Update root thread event only if the user have participated in
+        val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
+                realm = realm,
+                roomId = roomId,
+                rootThreadEventId = rootThreadEventId,
+                senderId = currentUserId
+        )
+        val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst()
+
+        if (isUserParticipating) {
+            rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
+        }
 
-            if (userMentionsList.contains(eventId)) {
-                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
-            }
+        if (userMentionsList.contains(rootThreadEventId)) {
+            rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
index 2be4510b6f..3b71ae3dea 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
@@ -50,7 +50,7 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(
                 .mapNotNull {
                     val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
                             ?: return@mapNotNull null
-                    ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
+                    ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong(), it.threadId)
                 }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
index 9623c95359..cedd5e7424 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
@@ -26,6 +26,7 @@ internal open class ReadReceiptEntity(
         var eventId: String = "",
         var roomId: String = "",
         var userId: String = "",
+        var threadId: String? = null,
         var originServerTs: Double = 0.0
 ) : RealmObject() {
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
index c8f22dc2cc..1deca47b70 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
@@ -20,6 +20,7 @@ import io.realm.RealmObject
 import io.realm.RealmResults
 import io.realm.annotations.Index
 import io.realm.annotations.LinkingObjects
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.extensions.assertIsManaged
 
 internal open class TimelineEventEntity(
@@ -52,3 +53,7 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
     }
     deleteFromRealm()
 }
+
+internal fun TimelineEventEntity.getThreadId(): String {
+    return root?.rootThreadEventId ?: ReadService.THREAD_ID_MAIN
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
index 0b0f01a67d..ebfe23105e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
@@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.database.query
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.getThreadId
 
 internal fun isEventRead(
         realmConfiguration: RealmConfiguration,
         userId: String?,
         roomId: String?,
-        eventId: String?
+        eventId: String?,
+        shouldCheckIfReadInEventsThread: Boolean
 ): Boolean {
     if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
         return false
@@ -45,7 +48,8 @@ internal fun isEventRead(
             eventToCheck.root?.sender == userId -> true
             // If new event exists and the latest event is from ourselves we can infer the event is read
             latestEventIsFromSelf(realm, roomId, userId) -> true
-            eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true
+            eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, null) -> true
+            (shouldCheckIfReadInEventsThread && eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, eventToCheck.getThreadId())) -> true
             else -> false
         }
     }
@@ -54,27 +58,33 @@ internal fun isEventRead(
 private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
         ?.root?.sender == userId
 
-private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean {
-    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt ->
+private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): Boolean {
+    val isMoreRecent = ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()?.let { readReceipt ->
         val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
         readReceiptEvent?.isMoreRecentThan(this)
     } ?: false
+    return isMoreRecent
 }
 
 /**
  * Missing events can be caused by the latest timeline chunk no longer contain an older event or
  * by fast lane eagerly displaying events before the database has finished updating.
  */
-private fun hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean {
-    return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId)
+private fun hasReadMissingEvent(realm: Realm,
+                                latestChunkEntity: ChunkEntity,
+                                roomId: String,
+                                userId: String,
+                                eventId: String,
+                                threadId: String? = ReadService.THREAD_ID_MAIN): Boolean {
+    return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId)
 }
 
 private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean {
     return ChunkEntity.findIncludingEvent(this, eventId) != null
 }
 
-private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean {
-    return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let {
+private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean {
+    return ReadReceiptEntity.where(this, roomId = roomId, userId = userId, threadId = threadId).findFirst()?.let {
         latestChunkEntity.timelineEvents.find(it.eventId)
     } != null
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
index 170814d3f2..0f9f56b938 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
@@ -20,12 +20,20 @@ import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.kotlin.createObject
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields
 
-internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery {
+internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String, threadId: String?): RealmQuery {
     return realm.where()
-            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId))
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, threadId))
+}
+
+internal fun ReadReceiptEntity.Companion.forMainTimelineWhere(realm: Realm, roomId: String, userId: String): RealmQuery {
+    return realm.where()
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, ReadService.THREAD_ID_MAIN))
+            .or()
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, null))
 }
 
 internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery {
@@ -38,23 +46,37 @@ internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: Strin
             .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
 }
 
-internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
+internal fun ReadReceiptEntity.Companion.createUnmanaged(
+        roomId: String,
+        eventId: String,
+        userId: String,
+        threadId: String?,
+        originServerTs: Double
+): ReadReceiptEntity {
     return ReadReceiptEntity().apply {
-        this.primaryKey = "${roomId}_$userId"
+        this.primaryKey = buildPrimaryKey(roomId, userId, threadId)
         this.eventId = eventId
         this.roomId = roomId
         this.userId = userId
+        this.threadId = threadId
         this.originServerTs = originServerTs
     }
 }
 
-internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
-    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
-            ?: realm.createObject(buildPrimaryKey(roomId, userId))
+internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String, threadId: String?): ReadReceiptEntity {
+    return ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()
+            ?: realm.createObject(buildPrimaryKey(roomId, userId, threadId))
                     .apply {
                         this.roomId = roomId
                         this.userId = userId
+                        this.threadId = threadId
                     }
 }
 
-private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId"
+private fun buildPrimaryKey(roomId: String, userId: String, threadId: String?): String {
+    return if (threadId == null) {
+        "${roomId}_${userId}"
+    } else {
+        "${roomId}_${userId}_${threadId}"
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 9bcb7b8e4c..31bed90b62 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMembersRespon
 import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason
 import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody
 import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
+import org.matrix.android.sdk.internal.session.room.read.ReadBody
 import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
 import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody
 import org.matrix.android.sdk.internal.session.room.send.SendResponse
@@ -173,7 +174,7 @@ internal interface RoomAPI {
             @Path("roomId") roomId: String,
             @Path("receiptType") receiptType: String,
             @Path("eventId") eventId: String,
-            @Body body: JsonDict = emptyMap()
+            @Body body: ReadBody
     )
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
index b30c66c82e..36ec5e8dac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
@@ -30,17 +30,20 @@ import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
 import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
+import org.matrix.android.sdk.internal.database.query.forMainTimelineWhere
 import org.matrix.android.sdk.internal.database.query.isEventRead
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
 
 internal class DefaultReadService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @SessionDatabase private val monarchy: Monarchy,
         private val setReadMarkersTask: SetReadMarkersTask,
         private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
-        @UserId private val userId: String
+        @UserId private val userId: String,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource
 ) : ReadService {
 
     @AssistedFactory
@@ -48,17 +51,28 @@ internal class DefaultReadService @AssistedInject constructor(
         fun create(roomId: String): DefaultReadService
     }
 
-    override suspend fun markAsRead(params: ReadService.MarkAsReadParams) {
+    override suspend fun markAsRead(params: ReadService.MarkAsReadParams, mainTimeLineOnly: Boolean) {
+        val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
+            if (mainTimeLineOnly) ReadService.THREAD_ID_MAIN else null
+        } else {
+            null
+        }
         val taskParams = SetReadMarkersTask.Params(
                 roomId = roomId,
                 forceReadMarker = params.forceReadMarker(),
-                forceReadReceipt = params.forceReadReceipt()
+                forceReadReceipt = params.forceReadReceipt(),
+                readReceiptThreadId = readReceiptThreadId
         )
         setReadMarkersTask.execute(taskParams)
     }
 
-    override suspend fun setReadReceipt(eventId: String) {
-        val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId)
+    override suspend fun setReadReceipt(eventId: String, threadId: String) {
+        val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
+            threadId
+        } else {
+            null
+        }
+        val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId, readReceiptThreadId = readReceiptThreadId)
         setReadMarkersTask.execute(params)
     }
 
@@ -68,7 +82,8 @@ internal class DefaultReadService @AssistedInject constructor(
     }
 
     override fun isEventRead(eventId: String): Boolean {
-        return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId)
+        val shouldCheckIfReadInEventsThread = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true
+        return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId, shouldCheckIfReadInEventsThread)
     }
 
     override fun getReadMarkerLive(): LiveData> {
@@ -81,9 +96,9 @@ internal class DefaultReadService @AssistedInject constructor(
         }
     }
 
-    override fun getMyReadReceiptLive(): LiveData> {
+    override fun getMyReadReceiptLive(threadId: String?): LiveData> {
         val liveRealmData = monarchy.findAllMappedWithChanges(
-                { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) },
+                { ReadReceiptEntity.where(it, roomId = roomId, userId = userId, threadId = threadId) },
                 { it.eventId }
         )
         return Transformations.map(liveRealmData) {
@@ -94,10 +109,11 @@ internal class DefaultReadService @AssistedInject constructor(
     override fun getUserReadReceipt(userId: String): String? {
         var eventId: String? = null
         monarchy.doWithRealm {
-            eventId = ReadReceiptEntity.where(it, roomId = roomId, userId = userId)
+            eventId = ReadReceiptEntity.forMainTimelineWhere(it, roomId = roomId, userId = userId)
                     .findFirst()
                     ?.eventId
         }
+
         return eventId
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt
new file mode 100644
index 0000000000..9374de5d5f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.read
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class ReadBody(
+        @Json(name = "thread_id") val threadId: String?,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
index a124a8a4c2..8e7592a8b4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.read
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.isEventRead
@@ -45,8 +46,9 @@ internal interface SetReadMarkersTask : Task {
             val roomId: String,
             val fullyReadEventId: String? = null,
             val readReceiptEventId: String? = null,
+            val readReceiptThreadId: String? = null,
             val forceReadReceipt: Boolean = false,
-            val forceReadMarker: Boolean = false
+            val forceReadMarker: Boolean = false,
     )
 }
 
@@ -61,12 +63,14 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
         @UserId private val userId: String,
         private val globalErrorReceiver: GlobalErrorReceiver,
         private val clock: Clock,
+        private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
 ) : SetReadMarkersTask {
 
     override suspend fun execute(params: SetReadMarkersTask.Params) {
         val markers = mutableMapOf()
         Timber.v("Execute set read marker with params: $params")
         val latestSyncedEventId = latestSyncedEventId(params.roomId)
+        val readReceiptThreadId = params.readReceiptThreadId
         val fullyReadEventId = if (params.forceReadMarker) {
             latestSyncedEventId
         } else {
@@ -77,6 +81,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
         } else {
             params.readReceiptEventId
         }
+
         if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) {
             if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
                 Timber.w("Can't set read marker for local event $fullyReadEventId")
@@ -84,8 +89,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 markers[READ_MARKER] = fullyReadEventId
             }
         }
+
+        val shouldCheckIfReadInEventsThread = readReceiptThreadId != null &&
+                homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
+
         if (readReceiptEventId != null &&
-                !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) {
+                !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId, shouldCheckIfReadInEventsThread)) {
             if (LocalEcho.isLocalEchoId(readReceiptEventId)) {
                 Timber.w("Can't set read receipt for local event $readReceiptEventId")
             } else {
@@ -95,7 +104,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
 
         val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId
         if (markers.isNotEmpty() || shouldUpdateRoomSummary) {
-            updateDatabase(params.roomId, markers, shouldUpdateRoomSummary)
+            updateDatabase(params.roomId, readReceiptThreadId, markers, shouldUpdateRoomSummary)
         }
         if (markers.isNotEmpty()) {
             executeRequest(
@@ -104,7 +113,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
             ) {
                 if (markers[READ_MARKER] == null) {
                     if (readReceiptEventId != null) {
-                        roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId)
+                        val readBody = ReadBody(threadId = params.readReceiptThreadId)
+                        roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId, readBody)
                     }
                 } else {
                     // "m.fully_read" value is mandatory to make this call
@@ -119,7 +129,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId
             }
 
-    private suspend fun updateDatabase(roomId: String, markers: Map, shouldUpdateRoomSummary: Boolean) {
+    private suspend fun updateDatabase(roomId: String, readReceiptThreadId: String?, markers: Map, shouldUpdateRoomSummary: Boolean) {
         monarchy.awaitTransaction { realm ->
             val readMarkerId = markers[READ_MARKER]
             val readReceiptId = markers[READ_RECEIPT]
@@ -127,7 +137,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId))
             }
             if (readReceiptId != null) {
-                val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis())
+                val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, readReceiptThreadId, clock.epochMillis())
                 readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null)
             }
             if (shouldUpdateRoomSummary) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index 7c83a4afa7..21a0862c65 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
@@ -75,7 +76,8 @@ internal class RoomSummaryUpdater @Inject constructor(
         private val roomAvatarResolver: RoomAvatarResolver,
         private val eventDecryptor: EventDecryptor,
         private val crossSigningService: DefaultCrossSigningService,
-        private val roomAccountDataDataSource: RoomAccountDataDataSource
+        private val roomAccountDataDataSource: RoomAccountDataDataSource,
+        private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
 ) {
 
     fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
@@ -151,9 +153,11 @@ internal class RoomSummaryUpdater @Inject constructor(
             latestPreviewableEvent.attemptToDecrypt()
         }
 
+        val shouldCheckIfReadInEventsThread = homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
+
         roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
                 // avoid this call if we are sure there are unread events
-                latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false
+                latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId, shouldCheckIfReadInEventsThread) } ?: false
 
         roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId))
         roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index c380ccf14f..0854cc5cf4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -411,7 +411,7 @@ internal class DefaultTimeline(
     private fun ensureReadReceiptAreLoaded(realm: Realm) {
         readReceiptHandler.getContentFromInitSync(roomId)
                 ?.also {
-                    Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId")
+                    Timber.d("INIT_SYNC Insert when opening timeline RR for room $roomId")
                 }
                 ?.let { readReceiptContent ->
                     realm.executeTransactionAsync {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
index 7329611a01..7f12ce653c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
@@ -33,10 +33,11 @@ import javax.inject.Inject
 // value : dict key $UserId
 //              value dict key ts
 //                    dict value ts value
-internal typealias ReadReceiptContent = Map>>>
+internal typealias ReadReceiptContent = Map>>>
 
 private const val READ_KEY = "m.read"
 private const val TIMESTAMP_KEY = "ts"
+private const val THREAD_ID_KEY = "thread_id"
 
 internal class ReadReceiptHandler @Inject constructor(
         private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
@@ -47,14 +48,19 @@ internal class ReadReceiptHandler @Inject constructor(
         fun createContent(
                 userId: String,
                 eventId: String,
+                threadId: String?,
                 currentTimeMillis: Long
         ): ReadReceiptContent {
+            val userReadReceipt = mutableMapOf(
+                    TIMESTAMP_KEY to currentTimeMillis.toDouble(),
+            )
+            threadId?.let {
+                userReadReceipt.put(THREAD_ID_KEY, threadId)
+            }
             return mapOf(
                     eventId to mapOf(
                             READ_KEY to mapOf(
-                                    userId to mapOf(
-                                            TIMESTAMP_KEY to currentTimeMillis.toDouble()
-                                    )
+                                    userId to userReadReceipt
                             )
                     )
             )
@@ -98,8 +104,9 @@ internal class ReadReceiptHandler @Inject constructor(
             val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId)
 
             for ((userId, paramsDict) in userIdsDict) {
-                val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
-                val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts)
+                val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0
+                val threadId = paramsDict[THREAD_ID_KEY] as String?
+                val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts)
                 readReceiptsSummary.readReceipts.add(receiptEntity)
             }
             readReceiptSummaries.add(readReceiptsSummary)
@@ -115,7 +122,7 @@ internal class ReadReceiptHandler @Inject constructor(
     ) {
         // First check if we have data from init sync to handle
         getContentFromInitSync(roomId)?.let {
-            Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId")
+            Timber.d("INIT_SYNC Insert during incremental sync RR for room $roomId")
             doIncrementalSyncStrategy(realm, roomId, it)
             aggregator?.ephemeralFilesToDelete?.add(roomId)
         }
@@ -132,8 +139,9 @@ internal class ReadReceiptHandler @Inject constructor(
                     }
 
             for ((userId, paramsDict) in userIdsDict) {
-                val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
-                val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId)
+                val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0
+                val threadId = paramsDict[THREAD_ID_KEY] as String?
+                val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId)
                 // ensure new ts is superior to the previous one
                 if (ts > receiptEntity.originServerTs) {
                     ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index ef238d56e6..02782783b8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -217,7 +217,7 @@ class TimelineViewModel @AssistedInject constructor(
         observePowerLevel()
         setupPreviewUrlObservers()
         viewModelScope.launch(Dispatchers.IO) {
-            tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
+            tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = true) }
         }
         // Inform the SDK that the room is displayed
         viewModelScope.launch(Dispatchers.IO) {
@@ -1103,7 +1103,8 @@ class TimelineViewModel @AssistedInject constructor(
                     }
                     bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId ->
                         session.coroutineScope.launch {
-                            tryOrNull { room.readService().setReadReceipt(eventId) }
+                            val threadId = initialState.rootThreadEventId ?: ReadService.THREAD_ID_MAIN
+                            tryOrNull { room.readService().setReadReceipt(eventId, threadId = threadId) }
                         }
                     }
                 }
@@ -1121,7 +1122,7 @@ class TimelineViewModel @AssistedInject constructor(
         if (room == null) return
         setState { copy(unreadState = UnreadState.HasNoUnread) }
         viewModelScope.launch {
-            tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH) }
+            tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.BOTH, mainTimeLineOnly = true) }
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 1f079e420b..f845a42dcd 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -74,6 +74,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import timber.log.Timber
@@ -516,7 +517,7 @@ class TimelineEventController @Inject constructor(
                         event.eventId,
                         readReceipts,
                         callback,
-                        partialState.isFromThreadTimeline()
+                        partialState.isFromThreadTimeline(),
                 ),
                 formattedDayModel = formattedDayModel,
                 mergedHeaderModel = mergedHeaderModel
@@ -559,7 +560,7 @@ class TimelineEventController @Inject constructor(
             val event = itr.previous()
             timelineEventsGroups.addOrIgnore(event)
             val currentReadReceipts = ArrayList(event.readReceipts).filter {
-                it.roomMember.userId != session.myUserId
+                it.roomMember.userId != session.myUserId && it.isVisibleInThisThread()
             }
             if (timelineEventVisibilityHelper.shouldShowEvent(
                             timelineEvent = event,
@@ -577,6 +578,14 @@ class TimelineEventController @Inject constructor(
         }
     }
 
+    private fun ReadReceipt.isVisibleInThisThread(): Boolean {
+        return if (partialState.isFromThreadTimeline()) {
+            this.threadId == partialState.rootThreadEventId
+        } else {
+            this.threadId == null || this.threadId == ReadService.THREAD_ID_MAIN
+        }
+    }
+
     private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
         val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
         return DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
index 6a711ec2dc..8607af6891 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt
@@ -21,16 +21,20 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
 import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
 import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
 import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
 import javax.inject.Inject
 
-class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
+class ReadReceiptsItemFactory @Inject constructor(
+        private val avatarRenderer: AvatarRenderer,
+        private val session: Session
+) {
 
     fun create(
             eventId: String,
             readReceipts: List,
             callback: TimelineEventController.Callback?,
-            isFromThreadTimeLine: Boolean
+            isFromThreadTimeLine: Boolean,
     ): ReadReceiptsItem? {
         if (readReceipts.isEmpty()) {
             return null
@@ -40,12 +44,13 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
                     ReadReceiptData(it.roomMember.userId, it.roomMember.avatarUrl, it.roomMember.displayName, it.originServerTs)
                 }
                 .sortedByDescending { it.timestamp }
+        val threadReadReceiptsSupported = session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
         return ReadReceiptsItem_()
                 .id("read_receipts_$eventId")
                 .eventId(eventId)
                 .readReceipts(readReceiptsData)
                 .avatarRenderer(avatarRenderer)
-                .shouldHideReadReceipts(isFromThreadTimeLine)
+                .shouldHideReadReceipts(isFromThreadTimeLine && !threadReadReceiptsSupported)
                 .clickListener {
                     callback?.onReadReceiptsClicked(readReceiptsData)
                 }
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
index 3fe0898eb4..180351f806 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
@@ -109,7 +109,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
             val room = session.getRoom(roomId)
             if (room != null) {
                 session.coroutineScope.launch {
-                    tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
+                    tryOrNull { room.readService().markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, mainTimeLineOnly = false) }
                 }
             }
         }

From 53cd55df6547cf3979c90d96954f030fd5a04115 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 25 Nov 2022 10:09:50 +0100
Subject: [PATCH 317/679] Bump wysiwyg from 0.7.0 to 0.7.0.1 (#7636)

Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.7.0 to 0.7.0.1.
- [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases)
- [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/matrix-org/matrix-wysiwyg/commits)

---
updated-dependencies:
- dependency-name: io.element.android:wysiwyg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index ac65035b60..93098952e6 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -98,7 +98,7 @@ ext.libs = [
         ],
         element     : [
                 'opusencoder'             : "io.element.android:opusencoder:1.1.0",
-                'wysiwyg'                 : "io.element.android:wysiwyg:0.7.0"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.7.0.1"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",

From ce6efa1f72def2a2676137e3eca6cf1e0f51ff05 Mon Sep 17 00:00:00 2001
From: Christina Klaas 
Date: Fri, 25 Nov 2022 11:06:58 +0000
Subject: [PATCH 318/679] Translated using Weblate (German)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
---
 .../src/main/res/values-de/strings.xml        | 28 +++++++++----------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml
index 4218f19f9f..ba0db71024 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -2232,13 +2232,13 @@
         %1$s weitere Optionen benötigt
     
     Frage darf nicht leer sein
-    ABSTIMMUNG ERSTELLEN
+    Umfrage erstellen
     NEUE OPTION
     Option %1$d
     Optionen hinzufügen
     Frage oder Thema
     Abstimmungsthema oder Frage
-    Abstimmung erstellen
+    Umfrage erstellen
     Umfrage
     Auffindungseinstellungen öffnen
     Sitzung abgemeldet!
@@ -2306,21 +2306,21 @@
     Richtlinie deines Identitäts-Servers
     Richtlinie deines Heim-Servers
     Richtlinie von ${app_name}
-    Abstimmung erstellen
+    Umfrage erstellen
     Kontakte öffnen
     Sticker verschicken
     Datei hochladen
     Verschicke Fotos und Videos
     Kamera öffnen
-    Willst du diese Abstimmung wirklich entfernen\? Du wirst sie nicht wiederherstellen können.
-    Abstimmung entfernen
-    Abstimmung beendet
+    Willst du diese Umfrage wirklich entfernen\? Du wirst sie nicht wiederherstellen können.
+    Umfrage entfernen
+    Umfrage beendet
     Stimme abgegeben
-    Abstimmung beenden
+    Umfrage beenden
     Dies verhindert, dass andere Personen abstimmen können, und zeigt die Endergebnisse der Umfrage an.
-    Diese Abstimmung beenden\?
+    Diese Umfrage beenden\?
     Gewinneroption
-    Abstimmung beenden
+    Umfrage beenden
     
         Endgültiges Ergebnis basiert auf %1$d Stimme
         Endgültiges Ergebnis basiert auf %1$d Stimmen
@@ -2333,11 +2333,11 @@
     ${app_name} konnte nicht auf deinen Standort zugreifen
     Standort
     Die Ergebnisse werden erst sichtbar, sobald du die Umfrage beendest
-    Abgeschlossene Abstimmung
+    Abgeschlossene Umfrage
     Abstimmende können die Ergebnisse nach Stimmabgabe sehen
-    Laufende Abstimmung
-    Abstimmungsart
-    Abstimmung bearbeiten
+    Laufende Umfrage
+    Umfragetyp
+    Umfrage bearbeiten
     Keine Stimmen abgegeben
     Konto erstellen
     Kommunikation für dein Team.
@@ -2527,7 +2527,7 @@
     Server-Richtlinien
     Folge den Anweisungen, die an %s gesendet wurden
     E-Mail bestätigen
-    Ergebnisse werden nach Abschluss der Abstimmung sichtbar sein
+    Ergebnisse werden nach Abschluss der Umfrage sichtbar sein
     Prüfe deine E-Mails.
     Passwort zurücksetzen
     Gib mindestens 8 Zeichen ein.

From 62bc22a03a8182a3aa3fa44e6d63d834c807ea00 Mon Sep 17 00:00:00 2001
From: Vri 
Date: Wed, 23 Nov 2022 16:12:42 +0000
Subject: [PATCH 319/679] Translated using Weblate (German)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
---
 .../ui-strings/src/main/res/values-de/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml
index ba0db71024..8eb6ba3da3 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -2852,4 +2852,17 @@
     
     Abmelden
     %1$s übrig
+    Zitieren
+    Bearbeiten
+    erstellte eine Abstimmung.
+    sandte einen Sticker.
+    sandte ein Video.
+    sandte ein Bild.
+    sandte eine Sprachnachricht.
+    sandte eine Audiodatei.
+    sandte eine Datei.
+    Als Antwort auf
+    %s antworten
+    IP-Adresse ausblenden
+    IP-Adresse anzeigen
 
\ No newline at end of file

From f1251140f5b9cb84e52cec6b75d70cfbcbdd32d8 Mon Sep 17 00:00:00 2001
From: waclaw66 
Date: Wed, 23 Nov 2022 15:57:46 +0000
Subject: [PATCH 320/679] Translated using Weblate (Czech)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/
---
 .../ui-strings/src/main/res/values-cs/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml
index 5ee8a6fd32..f260a129fc 100644
--- a/library/ui-strings/src/main/res/values-cs/strings.xml
+++ b/library/ui-strings/src/main/res/values-cs/strings.xml
@@ -2909,4 +2909,17 @@
     
     Odhlásit se
     zbývá %1$s
+    vytvořil hlasování.
+    poslal nálepku.
+    poslal video.
+    poslal obrázek.
+    poslal hlasovou zprávu.
+    poslal zvukový soubor.
+    odeslal soubor.
+    V odpovědi na
+    Skrýt IP adresu
+    Zobrazit IP adresu
+    Citace
+    Odpovídám na %s
+    Úpravy
 
\ No newline at end of file

From cc951969de55ab1f66576db167d18302f90e471d Mon Sep 17 00:00:00 2001
From: Christina Klaas 
Date: Fri, 25 Nov 2022 11:07:07 +0000
Subject: [PATCH 321/679] Translated using Weblate (German)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
---
 library/ui-strings/src/main/res/values-de/strings.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml
index 8eb6ba3da3..75d77ea718 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -2854,7 +2854,7 @@
     %1$s übrig
     Zitieren
     Bearbeiten
-    erstellte eine Abstimmung.
+    erstellte eine Umfrage.
     sandte einen Sticker.
     sandte ein Video.
     sandte ein Bild.

From b0ec6abbea180145a43fe0a043574944e9df14d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= 
Date: Wed, 23 Nov 2022 17:37:04 +0000
Subject: [PATCH 322/679] Translated using Weblate (Estonian)

Currently translated at 99.6% (2548 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/et/
---
 .../ui-strings/src/main/res/values-et/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml
index cf5f23a240..156221379d 100644
--- a/library/ui-strings/src/main/res/values-et/strings.xml
+++ b/library/ui-strings/src/main/res/values-et/strings.xml
@@ -2844,4 +2844,17 @@
     
     Logi välja
     jäänud %1$s
+    Muudan sõnumit
+    Vastan sõnumile %s
+    Tsiteerides
+    Näita IP-aadressi
+    Peida IP-aadress
+    Vastuseks kasutajale
+    saatis faili.
+    saatis helifaili.
+    saatis häälsõnumi.
+    saatis pildi.
+    saatis video.
+    saatis kleepsu.
+    koostas küsitluse.
 
\ No newline at end of file

From 205db447e63e48051d42a7e727ef300da04d0340 Mon Sep 17 00:00:00 2001
From: Danial Behzadi 
Date: Thu, 24 Nov 2022 09:32:25 +0000
Subject: [PATCH 323/679] Translated using Weblate (Persian)

Currently translated at 99.6% (2546 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/
---
 .../ui-strings/src/main/res/values-fa/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml
index fb3651dfa2..cc8d60a87b 100644
--- a/library/ui-strings/src/main/res/values-fa/strings.xml
+++ b/library/ui-strings/src/main/res/values-fa/strings.xml
@@ -2831,4 +2831,17 @@
         خروج از %1$d نشست
     
     %1$s مانده
+    پیام صوتی‌ای فرستاد.
+    پروندهٔ صوتی‌ای فرستاد.
+    نظرسنجی‌ای ایجاد کرد.
+    عکس‌برگردانی فرستاد.
+    ویدیویی فرستاد.
+    تصویری فرستاد.
+    پرونده‌ای فرستاد.
+    در پاسخ به
+    نهفتن نشانی آی‌پی
+    نمایش نشانی آی‌پی
+    نقل کردن
+    پاسخ دادن به %s
+    ویرایش کردن
 
\ No newline at end of file

From eb2910401f4a0efe5a7a6dca5302f9788a4eeafc Mon Sep 17 00:00:00 2001
From: Linerly 
Date: Wed, 23 Nov 2022 23:23:35 +0000
Subject: [PATCH 324/679] Translated using Weblate (Indonesian)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/id/
---
 .../ui-strings/src/main/res/values-in/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml
index 7828ec8c13..ce9e524067 100644
--- a/library/ui-strings/src/main/res/values-in/strings.xml
+++ b/library/ui-strings/src/main/res/values-in/strings.xml
@@ -2799,4 +2799,17 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
     
     Keluarkan
     %1$s tersisa
+    membuat pemungutan suara.
+    mengirim stiker.
+    mengirim video.
+    mengirim gambar.
+    mengirim file.
+    mengirim file audio.
+    mengirim pesan suara.
+    Membalas ke
+    Sembunyikan alamat IP
+    Mengutip
+    Mengedit
+    Tampilkan alamat IP
+    Membalas ke %s
 
\ No newline at end of file

From f5534363b993f3adbe01451bbb4e2294f8d17b10 Mon Sep 17 00:00:00 2001
From: random 
Date: Wed, 23 Nov 2022 15:44:40 +0000
Subject: [PATCH 325/679] Translated using Weblate (Italian)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/it/
---
 .../ui-strings/src/main/res/values-it/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml
index 2322561d05..d244f26a43 100644
--- a/library/ui-strings/src/main/res/values-it/strings.xml
+++ b/library/ui-strings/src/main/res/values-it/strings.xml
@@ -2844,4 +2844,17 @@
     Manda avanti di 30 secondi
     Manda indietro di 30 secondi
     %1$s rimasti
+    creato un sondaggio.
+    inviato un adesivo.
+    inviato un video.
+    inviata un\'immagine.
+    inviato un messaggio vocale.
+    inviato un file audio.
+    inviato un file.
+    In risposta a
+    Nascondi indirizzo IP
+    Mostra indirizzo IP
+    Citazione
+    Risposta a %s
+    Modifica
 
\ No newline at end of file

From e1dc05d5edd6f2e9d30784b336157f09705122e6 Mon Sep 17 00:00:00 2001
From: lvre <7uu3qrbvm@relay.firefox.com>
Date: Fri, 25 Nov 2022 07:02:00 +0000
Subject: [PATCH 326/679] Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/
---
 .../src/main/res/values-pt-rBR/strings.xml          | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
index a916255d6f..8baba5df53 100644
--- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
+++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
@@ -2853,4 +2853,17 @@
     
     Fazer signout
     %1$s restando
+    criou uma sondagem.
+    enviou um sticker.
+    enviou um vídeo.
+    enviou uma imagem.
+    enviou uma mensagem de voz.
+    enviou um arquivo de áudio.
+    enviou um arquivo.
+    Em resposta a
+    Esconder endereço de IP
+    Mostrar endereço de IP
+    Citando
+    Respondendo a %s
+    Editando
 
\ No newline at end of file

From 6b9381f5a36316f1700b69cba822276e57c2a70b Mon Sep 17 00:00:00 2001
From: Jozef Gaal 
Date: Wed, 23 Nov 2022 19:22:12 +0000
Subject: [PATCH 327/679] Translated using Weblate (Slovak)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/
---
 .../ui-strings/src/main/res/values-sk/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml
index acd69a2024..078ffc44eb 100644
--- a/library/ui-strings/src/main/res/values-sk/strings.xml
+++ b/library/ui-strings/src/main/res/values-sk/strings.xml
@@ -2909,4 +2909,17 @@
     
     Odhlásiť sa
     Ostáva %1$s
+    Cituje
+    vytvoril/a anketu.
+    poslal/a nálepku.
+    poslal/a video.
+    poslal/a obrázok.
+    poslal/a zvukovú správu.
+    poslal/a zvukový súbor.
+    poslal súbor.
+    V odpovedi na
+    Skryť IP adresu
+    Zobraziť IP adresu
+    Odpoveď na %s
+    Úprava
 
\ No newline at end of file

From aa2c6e175b2e4173dc48b76401705e232721ad02 Mon Sep 17 00:00:00 2001
From: Besnik Bleta 
Date: Thu, 24 Nov 2022 17:20:32 +0000
Subject: [PATCH 328/679] Translated using Weblate (Albanian)

Currently translated at 99.3% (2540 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/
---
 .../src/main/res/values-sq/strings.xml        | 29 +++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml
index 773454c39f..800ec17dcf 100644
--- a/library/ui-strings/src/main/res/values-sq/strings.xml
+++ b/library/ui-strings/src/main/res/values-sq/strings.xml
@@ -2822,4 +2822,33 @@
     Ky kod QR duket i formuar keq. Ju lutemi, provoni ta verifikoni me tjetër metodë.
     🔒 Keni aktivizuar fshehtëzim për sesionie të verifikuar vetëm për krejt dhomat, që nga Rregullime Sigurie.
     Luaj figura të animuara te rrjedha kohora sapo zënë të duken
+    krijoi një pyetësor.
+    dërgoi një ngjitës.
+    dërgoi një video.
+    dërgoi një figurë.
+    dërgoi një mesazh zanor.
+    dërgoi një kartelë audio.
+    dërgoi një kartelë.
+    Në përgjigje të
+    Hyni/Dilni nga mënyra “Sa krejt ekrani”
+    Sesionet e verifikuar janë kudo ku përdorni këtë llogari pas dhënies së frazëkalimit tuaj, apo ripohimit të identitetit tuaj me një sesion tjetër të verifikuar.
+\n
+\nKjo do të thotë se keni krejt kyçet e nevojshëm për të shkyçur mesazhet tuaj të fshehtëzuar dhe për të ripohuar se e besoni këtë sesion.
+    Fshihe adresën IP
+    Shfaq adresë IP
+    
+        Dilni nga %1$d sesion
+        Dilni nga %1$d sesione
+    
+    Dilni
+    Formatim teksti
+    Edhe %1$s
+    Jeni duke incizuar tashmë një transmetim zanor. Ju lutemi, që të nisni një të ri, përfundoni transmetimin tuaj aktual zanor.
+    Dikush tjetër është tashmë duke incizuar një transmetim zanor. Prisni që të përfundojë transmetimi zanor i tij, pa të filloni një të ri.
+    S’keni lejet e domosdoshme për të nisur një transmetim zanor në këtë dhomë. Lidhuni me një përgjegjës dhome që të përmirësojë lejet tuaja.
+    S’mund të niset një transmetim i ri zanor
+    Shtyrje përpara 30 sekonda
+    Kthim prapa 30 sekonda
+    Si përgjigje për %s
+    Aktivizo MD të lënë për më vonë
 
\ No newline at end of file

From c424a87d817cc5a29357273a5e039d1436672b0a Mon Sep 17 00:00:00 2001
From: Jeff Huang 
Date: Thu, 24 Nov 2022 02:36:14 +0000
Subject: [PATCH 329/679] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/
---
 .../src/main/res/values-zh-rTW/strings.xml          | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
index 30db508f83..dc5f6d85e3 100644
--- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
+++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
@@ -2797,4 +2797,17 @@
     
     登出
     剩餘 %1$s
+    已建立投票。
+    已傳送貼圖。
+    已傳送影片。
+    已傳送圖片。
+    已傳送語音訊息。
+    已傳送音訊檔。
+    已傳送檔案。
+    回覆給
+    隱藏 IP 位置
+    顯示 IP 位置
+    引用
+    回覆給 %s
+    正在編輯
 
\ No newline at end of file

From bc4ca1d33d061718cbc23445d0b8310b4676e7f3 Mon Sep 17 00:00:00 2001
From: Vri 
Date: Wed, 23 Nov 2022 15:46:01 +0000
Subject: [PATCH 330/679] Translated using Weblate (German)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/de/
---
 fastlane/metadata/android/de-DE/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/de-DE/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/de-DE/changelogs/40105080.txt b/fastlane/metadata/android/de-DE/changelogs/40105080.txt
new file mode 100644
index 0000000000..0422f9cd4f
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Die wichtigsten Änderungen in dieser Version: Fehlerbehebungen und Verbesserungen.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases

From fe8415d788f47afc324e334d33ae1dd8331511cb Mon Sep 17 00:00:00 2001
From: lvre <7uu3qrbvm@relay.firefox.com>
Date: Fri, 25 Nov 2022 06:59:02 +0000
Subject: [PATCH 331/679] Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/
---
 fastlane/metadata/android/pt-BR/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/pt-BR/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/pt-BR/changelogs/40105080.txt b/fastlane/metadata/android/pt-BR/changelogs/40105080.txt
new file mode 100644
index 0000000000..6e85b9ba6a
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Principais mudanças nesta versão: consertos de bugs e melhorias.
+Changelog completo: https://github.com/vector-im/element-android/releases

From 5811f1c77f02c46cfc092c20697d4f6a09764399 Mon Sep 17 00:00:00 2001
From: Jozef Gaal 
Date: Wed, 23 Nov 2022 19:19:35 +0000
Subject: [PATCH 332/679] Translated using Weblate (Slovak)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/
---
 fastlane/metadata/android/sk/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/sk/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/sk/changelogs/40105080.txt b/fastlane/metadata/android/sk/changelogs/40105080.txt
new file mode 100644
index 0000000000..56daa3b4b7
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: opravy chýb a vylepšenia.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases

From 0a77b8f7d772340955f12d98a836224786d05228 Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk 
Date: Thu, 24 Nov 2022 16:05:37 +0000
Subject: [PATCH 333/679] Translated using Weblate (Ukrainian)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/
---
 fastlane/metadata/android/uk/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/uk/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/uk/changelogs/40105080.txt b/fastlane/metadata/android/uk/changelogs/40105080.txt
new file mode 100644
index 0000000000..e6f6384a5f
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: усування вад і вдосконалення.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases

From 9b6c57f5fdddad2d3aa4d7afeb6d08c449d81791 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= 
Date: Wed, 23 Nov 2022 17:34:25 +0000
Subject: [PATCH 334/679] Translated using Weblate (Estonian)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/et/
---
 fastlane/metadata/android/et/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/et/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/et/changelogs/40105080.txt b/fastlane/metadata/android/et/changelogs/40105080.txt
new file mode 100644
index 0000000000..37b9a2cfe5
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: erinevate vigade parandused ja kohendused.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases

From ac24bee0a6ae2dab2b401b71c2eb694438069cad Mon Sep 17 00:00:00 2001
From: random 
Date: Wed, 23 Nov 2022 15:45:10 +0000
Subject: [PATCH 335/679] Translated using Weblate (Italian)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/it/
---
 fastlane/metadata/android/it-IT/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/it-IT/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/it-IT/changelogs/40105080.txt b/fastlane/metadata/android/it-IT/changelogs/40105080.txt
new file mode 100644
index 0000000000..a3d49ca1b7
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Modifiche principali in questa versione: correzione di errori e miglioramenti.
+Cronologia completa: https://github.com/vector-im/element-android/releases

From 78025ddc18b0d2a2f44c3e8b76ccd06db066d3ac Mon Sep 17 00:00:00 2001
From: Danial Behzadi 
Date: Thu, 24 Nov 2022 09:34:13 +0000
Subject: [PATCH 336/679] Translated using Weblate (Persian)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/fa/
---
 fastlane/metadata/android/fa/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/fa/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/fa/changelogs/40105080.txt b/fastlane/metadata/android/fa/changelogs/40105080.txt
new file mode 100644
index 0000000000..91385addde
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+تغییرات عمده در این نگارش: رفع اشکال‌ها و بهبود.
+گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases

From de9a6c6c34ae6c03d4d8be9236da9d9cb426b562 Mon Sep 17 00:00:00 2001
From: Jeff Huang 
Date: Thu, 24 Nov 2022 02:34:27 +0000
Subject: [PATCH 337/679] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/
---
 fastlane/metadata/android/zh-TW/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/zh-TW/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105080.txt b/fastlane/metadata/android/zh-TW/changelogs/40105080.txt
new file mode 100644
index 0000000000..2a368ec8be
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:臭蟲修復與改善。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases

From a5467062a154f101e0aec02dd0868ae7d3da878f Mon Sep 17 00:00:00 2001
From: waclaw66 
Date: Wed, 23 Nov 2022 15:58:50 +0000
Subject: [PATCH 338/679] Translated using Weblate (Czech)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/
---
 fastlane/metadata/android/cs-CZ/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105080.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105080.txt
new file mode 100644
index 0000000000..90210199a1
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: opravy různých chyb a vylepšení.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases

From 5bfb83985e9743f742c6601a87cb3fd660cf8729 Mon Sep 17 00:00:00 2001
From: Linerly 
Date: Wed, 23 Nov 2022 23:24:30 +0000
Subject: [PATCH 339/679] Translated using Weblate (Indonesian)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/id/
---
 fastlane/metadata/android/id/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/id/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/id/changelogs/40105080.txt b/fastlane/metadata/android/id/changelogs/40105080.txt
new file mode 100644
index 0000000000..8384716bbc
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: perbaikan kutu dan fitur
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases

From 821a5612358236a579a78b08296b9de2b347f4b7 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 25 Nov 2022 14:33:41 +0300
Subject: [PATCH 340/679] Add timeout preference for alert.

---
 .../src/main/java/im/vector/app/config/Config.kt   |  4 ++++
 .../java/im/vector/app/features/VectorFeatures.kt  |  2 ++
 .../app/features/settings/VectorPreferences.kt     | 14 +++++++++++++-
 3 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt
index c91987dbfd..fdc8e9f73b 100644
--- a/vector-config/src/main/java/im/vector/app/config/Config.kt
+++ b/vector-config/src/main/java/im/vector/app/config/Config.kt
@@ -16,6 +16,8 @@
 
 package im.vector.app.config
 
+import kotlin.time.Duration.Companion.days
+
 /**
  * Set of flags to configure the application.
  */
@@ -93,4 +95,6 @@ object Config {
      * Can be disabled by providing Analytics.Disabled
      */
     val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY")
+
+    val SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS = 7.days.inWholeMilliseconds // 1 Week
 }
diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
index 95cf272abd..28c2e37926 100644
--- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
+++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
@@ -44,6 +44,7 @@ interface VectorFeatures {
     fun isQrCodeLoginForAllServers(): Boolean
     fun isReciprocateQrCodeLogin(): Boolean
     fun isVoiceBroadcastEnabled(): Boolean
+    fun isUnverifiedSessionsAlertEnabled(): Boolean
 }
 
 class DefaultVectorFeatures : VectorFeatures {
@@ -63,4 +64,5 @@ class DefaultVectorFeatures : VectorFeatures {
     override fun isQrCodeLoginForAllServers(): Boolean = false
     override fun isReciprocateQrCodeLogin(): Boolean = false
     override fun isVoiceBroadcastEnabled(): Boolean = true
+    override fun isUnverifiedSessionsAlertEnabled(): Boolean = false
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index 447038d768..3f35080057 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -245,6 +245,8 @@ class VectorPreferences @Inject constructor(
         // This key will be used to enable user for displaying live user info or not.
         const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO"
 
+        const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS"
+
         // Possible values for TAKE_PHOTO_VIDEO_MODE
         const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
         const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
@@ -1238,7 +1240,17 @@ class VectorPreferences @Inject constructor(
 
     fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) {
         defaultPrefs.edit {
-            putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible)
+            putBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible)
+        }
+    }
+
+    fun getUnverifiedSessionsAlertLastShownMillis(): Long {
+        return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, 0)
+    }
+
+    fun setUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) {
+        defaultPrefs.edit {
+            putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, lastShownMillis)
         }
     }
 }

From 8835e4d25e8f0a3e55ef33cfe0d99834526e2561 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 25 Nov 2022 14:34:39 +0300
Subject: [PATCH 341/679] Create use case to decide to show alert.

---
 ...houldShowUnverifiedSessionsAlertUseCase.kt | 37 ++++++++++
 ...dShowUnverifiedSessionsAlertUseCaseTest.kt | 74 +++++++++++++++++++
 .../app/test/fakes/FakeVectorFeatures.kt      |  4 +
 .../app/test/fakes/FakeVectorPreferences.kt   |  4 +
 4 files changed, 119 insertions(+)
 create mode 100644 vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt

diff --git a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt
new file mode 100644
index 0000000000..0455b4399a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home
+
+import im.vector.app.config.Config
+import im.vector.app.core.time.Clock
+import im.vector.app.features.VectorFeatures
+import im.vector.app.features.settings.VectorPreferences
+import javax.inject.Inject
+
+class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor(
+        private val vectorFeatures: VectorFeatures,
+        private val vectorPreferences: VectorPreferences,
+        private val clock: Clock,
+) {
+
+    fun execute(): Boolean {
+        val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled()
+        val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis()
+        return isUnverifiedSessionsAlertEnabled &&
+                clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt
new file mode 100644
index 0000000000..cb4b8b2a1f
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home
+
+import im.vector.app.config.Config
+import im.vector.app.test.fakes.FakeClock
+import im.vector.app.test.fakes.FakeVectorFeatures
+import im.vector.app.test.fakes.FakeVectorPreferences
+import org.amshove.kluent.shouldBe
+import org.junit.Test
+
+private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS
+
+class ShouldShowUnverifiedSessionsAlertUseCaseTest {
+
+    private val fakeVectorFeatures = FakeVectorFeatures()
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeClock = FakeClock()
+
+    private val shouldShowUnverifiedSessionsAlertUseCase = ShouldShowUnverifiedSessionsAlertUseCase(
+            vectorFeatures = fakeVectorFeatures,
+            vectorPreferences = fakeVectorPreferences.instance,
+            clock = fakeClock,
+    )
+
+    @Test
+    fun `given the feature is disabled then the use case returns false`() {
+        fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(false)
+        fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L)
+
+        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false
+    }
+
+    @Test
+    fun `given the feature in enabled and there is not a saved preference then the use case returns true`() {
+        fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true)
+        fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L)
+        fakeClock.givenEpoch(AN_EPOCH + 1)
+
+        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true
+    }
+
+    @Test
+    fun `given the feature in enabled and last shown is a long time ago then the use case returns true`() {
+        fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true)
+        fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH)
+        fakeClock.givenEpoch(AN_EPOCH * 2 + 1)
+
+        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true
+    }
+
+    @Test
+    fun `given the feature in enabled and last shown is not a long time ago then the use case returns false`() {
+        fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true)
+        fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH)
+        fakeClock.givenEpoch(AN_EPOCH + 1)
+
+        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
index d989abc214..c3c2fa684f 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
@@ -50,4 +50,8 @@ class FakeVectorFeatures : VectorFeatures by spyk() {
     fun givenVoiceBroadcast(isEnabled: Boolean) {
         every { isVoiceBroadcastEnabled() } returns isEnabled
     }
+
+    fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) {
+        every { isUnverifiedSessionsAlertEnabled() } returns isEnabled
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index d89764a77e..101657b260 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -56,4 +56,8 @@ class FakeVectorPreferences {
     fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) {
         every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress
     }
+
+    fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) {
+        every { instance.getUnverifiedSessionsAlertLastShownMillis() } returns lastShownMillis
+    }
 }

From 9349b1ae15e9b323d2fd7b73d9bc48bc44f8a90b Mon Sep 17 00:00:00 2001
From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com>
Date: Fri, 25 Nov 2022 14:24:14 +0100
Subject: [PATCH 342/679] read receipt migration added (#7640)

---
 .../database/RealmSessionStoreMigration.kt    |  4 ++-
 .../database/migration/MigrateSessionTo044.kt | 29 +++++++++++++++++++
 2 files changed, 32 insertions(+), 1 deletion(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 388f962454..1529064b96 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -60,6 +60,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
 import org.matrix.android.sdk.internal.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
@@ -68,7 +69,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
 ) : MatrixRealmMigration(
         dbName = "Session",
-        schemaVersion = 43L,
+        schemaVersion = 44L,
 ) {
     /**
      * Forces all RealmSessionStoreMigration instances to be equal.
@@ -121,5 +122,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 41) MigrateSessionTo041(realm).perform()
         if (oldVersion < 42) MigrateSessionTo042(realm).perform()
         if (oldVersion < 43) MigrateSessionTo043(realm).perform()
+        if (oldVersion < 44) MigrateSessionTo044(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt
new file mode 100644
index 0000000000..2d3efc8338
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo044(realm: DynamicRealm) : RealmMigrator(realm, 44) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("ReadReceiptEntity")
+                ?.addField(ReadReceiptEntityFields.THREAD_ID, String::class.java)
+    }
+}

From f4b948af9d1103e3da9d6276895bf550d7d18a51 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 16 Nov 2022 10:12:27 +0100
Subject: [PATCH 343/679] Voice Broadcast - hide voice messages and state
 events behind hidden events

---
 .../detail/timeline/factory/MessageItemFactory.kt |  7 +++++--
 .../timeline/factory/VoiceBroadcastItemFactory.kt |  8 ++++++--
 .../timeline/format/NoticeEventFormatter.kt       |  4 +++-
 .../helper/TimelineEventVisibilityHelper.kt       | 15 +++++++++++++++
 4 files changed, 29 insertions(+), 5 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 373410775b..42e031a3c4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -45,6 +45,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio
 import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
 import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
 import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
+import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem_
 import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
@@ -324,9 +325,11 @@ class MessageItemFactory @Inject constructor(
             informationData: MessageInformationData,
             highlight: Boolean,
             attributes: AbsMessageItem.Attributes
-    ): MessageVoiceItem? {
+    ): BaseEventItem<*>? {
         // Do not display voice broadcast messages
-        if (params.event.root.asMessageAudioEvent().isVoiceBroadcast()) return null
+        if (params.event.root.asMessageAudioEvent().isVoiceBroadcast()) {
+            return noticeItemFactory.create(params)
+        }
 
         val fileUrl = getAudioFileUrl(messageContent, informationData)
         val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index e4f7bed72f..cc3a015120 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -23,6 +23,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide
 import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
 import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
 import im.vector.app.features.home.room.detail.timeline.item.AbsMessageVoiceBroadcastItem
+import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem
 import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
 import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
@@ -47,6 +48,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
         private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
         private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
         private val playbackTracker: AudioMessagePlaybackTracker,
+        private val noticeItemFactory: NoticeItemFactory,
 ) {
 
     fun create(
@@ -54,9 +56,11 @@ class VoiceBroadcastItemFactory @Inject constructor(
             messageContent: MessageVoiceBroadcastInfoContent,
             highlight: Boolean,
             attributes: AbsMessageItem.Attributes,
-    ): AbsMessageVoiceBroadcastItem<*>? {
+    ): BaseEventItem<*>? {
         // Only display item of the initial event with updated data
-        if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
+        if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) {
+            return noticeItemFactory.create(params)
+        }
 
         val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
         val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index 19f9fc17a3..def3fb1a44 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -21,6 +21,7 @@ import im.vector.app.R
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.roomprofile.permissions.RoleFormatter
 import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
 import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.extensions.appendNl
 import org.matrix.android.sdk.api.extensions.orFalse
@@ -108,7 +109,8 @@ class NoticeEventFormatter @Inject constructor(
             EventType.STICKER,
             in EventType.POLL_RESPONSE,
             in EventType.POLL_END,
-            in EventType.BEACON_LOCATION_DATA -> formatDebug(timelineEvent.root)
+            in EventType.BEACON_LOCATION_DATA,
+            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatDebug(timelineEvent.root)
             else -> {
                 Timber.v("Type $type not handled by this formatter")
                 null
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index d22b649b36..cce20d102c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -18,6 +18,11 @@ package im.vector.app.features.home.room.detail.timeline.helper
 
 import im.vector.app.core.extensions.localDateTime
 import im.vector.app.core.resources.UserPreferencesProvider
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
@@ -28,6 +33,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import javax.inject.Inject
 
@@ -246,6 +252,15 @@ class TimelineEventVisibilityHelper @Inject constructor(
             return !root.isRedacted()
         }
 
+        if (root.getClearType() == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO &&
+                root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState != VoiceBroadcastState.STARTED) {
+            return true
+        }
+
+        if (root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse()) {
+            return true
+        }
+
         return false
     }
 

From eb12b1c99bff4faa10e336e12969812667e7148f Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 18 Nov 2022 16:28:46 +0100
Subject: [PATCH 344/679] Use StableUnstableId object for some event types

---
 .../room/timeline/PollAggregationTest.kt      |  2 +-
 .../sdk/api/session/events/model/Event.kt     | 10 +++++-----
 .../sdk/api/session/events/model/EventType.kt | 19 +++++++------------
 .../room/summary/RoomSummaryConstants.kt      |  4 +++-
 .../session/room/timeline/TimelineEvent.kt    |  6 +++---
 .../session/call/CallEventProcessor.kt        |  5 ++---
 .../session/call/CallSignalingHandler.kt      |  3 +--
 .../pushrules/ProcessEventForPushTask.kt      |  4 ++--
 .../EventRelationsAggregationProcessor.kt     | 19 ++++++++++++-------
 .../GetActiveBeaconInfoForUserTask.kt         |  2 +-
 ...iveLocationShareRedactionEventProcessor.kt |  2 +-
 .../location/StartLiveLocationShareTask.kt    |  2 +-
 .../location/StopLiveLocationShareTask.kt     |  2 +-
 .../room/prune/RedactionEventProcessor.kt     |  6 +++---
 .../room/send/LocalEchoEventFactory.kt        | 10 +++++-----
 .../aggregation/poll/PollEventsTestData.kt    |  6 +++---
 ...faultGetActiveBeaconInfoForUserTaskTest.kt |  2 +-
 .../DefaultStartLiveLocationShareTaskTest.kt  |  2 +-
 .../DefaultStopLiveLocationShareTaskTest.kt   |  2 +-
 ...ocationShareRedactionEventProcessorTest.kt |  2 +-
 .../app/core/extensions/TimelineEvent.kt      |  2 +-
 .../home/room/detail/TimelineFragment.kt      |  2 +-
 .../action/CheckIfCanRedactEventUseCase.kt    |  4 ++--
 .../action/CheckIfCanReplyEventUseCase.kt     |  2 +-
 .../action/MessageActionsViewModel.kt         | 14 +++++++-------
 .../timeline/factory/TimelineItemFactory.kt   | 10 +++++-----
 .../format/DisplayableEventFormatter.kt       | 16 ++++++++--------
 .../timeline/format/NoticeEventFormatter.kt   |  6 +++---
 .../helper/TimelineDisplayableEvents.kt       |  6 +++---
 .../helper/TimelineEventVisibilityHelper.kt   |  2 +-
 .../style/TimelineMessageLayoutFactory.kt     |  6 ++++--
 .../location/LocationSharingViewModel.kt      |  2 +-
 .../notifications/NotifiableEventResolver.kt  |  2 +-
 .../CheckIfCanRedactEventUseCaseTest.kt       |  9 +++++++--
 .../action/CheckIfCanReplyEventUseCaseTest.kt |  2 +-
 .../test/fakes/FakeCreatePollViewStates.kt    |  6 +++---
 36 files changed, 104 insertions(+), 97 deletions(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
index a37d2ce015..a52e3cd7c7 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
@@ -66,7 +66,7 @@ class PollAggregationTest : InstrumentedTest {
 
         val aliceEventsListener = object : Timeline.Listener {
             override fun onTimelineUpdated(snapshot: List) {
-                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
+                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START.values }?.let { pollEvent ->
                     val pollEventId = pollEvent.eventId
                     val pollContent = pollEvent.root.content?.toModel()
                     val pollSummary = pollEvent.annotations?.pollResponseSummary
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 1e32437727..f84e9858a5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -390,11 +390,11 @@ fun Event.isLocationMessage(): Boolean {
     }
 }
 
-fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
+fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values
 
 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
 
-fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO
+fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO.values
 
 fun Event.getRelationContent(): RelationDefaultContent? {
     return if (isEncrypted()) {
@@ -404,7 +404,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
             // Special cases when there is only a local msgtype for some event types
             when (getClearType()) {
                 EventType.STICKER -> getClearContent().toModel()?.relatesTo
-                in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel()?.relatesTo
+                in EventType.BEACON_LOCATION_DATA.values -> getClearContent().toModel()?.relatesTo
                 else -> getClearContent()?.get("m.relates_to")?.toContent().toModel()
             }
         }
@@ -451,7 +451,7 @@ fun Event.getPollContent(): MessagePollContent? {
 }
 
 fun Event.supportsNotification() =
-        this.getClearType() in EventType.MESSAGE + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+        this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.STATE_ROOM_BEACON_INFO.values
 
 fun Event.isContentReportable() =
-        this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO
+        this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index 3ad4f3a87f..e5c14afa90 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -49,11 +49,10 @@ object EventType {
     const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
     const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
     const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
-    val STATE_ROOM_BEACON_INFO = listOf("org.matrix.msc3672.beacon_info", "m.beacon_info")
-    val BEACON_LOCATION_DATA = listOf("org.matrix.msc3672.beacon", "m.beacon")
+    val STATE_ROOM_BEACON_INFO = StableUnstableId(stable = "m.beacon_info", unstable = "org.matrix.msc3672.beacon_info")
+    val BEACON_LOCATION_DATA = StableUnstableId(stable = "m.beacon", unstable = "org.matrix.msc3672.beacon")
 
     const val STATE_SPACE_CHILD = "m.space.child"
-
     const val STATE_SPACE_PARENT = "m.space.parent"
 
     /**
@@ -81,8 +80,7 @@ object EventType {
     const val CALL_NEGOTIATE = "m.call.negotiate"
     const val CALL_REJECT = "m.call.reject"
     const val CALL_HANGUP = "m.call.hangup"
-    const val CALL_ASSERTED_IDENTITY = "m.call.asserted_identity"
-    const val CALL_ASSERTED_IDENTITY_PREFIX = "org.matrix.call.asserted_identity"
+    val CALL_ASSERTED_IDENTITY = StableUnstableId(stable = "m.call.asserted_identity", unstable = "org.matrix.call.asserted_identity")
 
     // This type is not processed by the client, just sent to the server
     const val CALL_REPLACES = "m.call.replaces"
@@ -90,10 +88,7 @@ object EventType {
     // Key share events
     const val ROOM_KEY_REQUEST = "m.room_key_request"
     const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
-    val ROOM_KEY_WITHHELD = StableUnstableId(
-            stable = "m.room_key.withheld",
-            unstable = "org.matrix.room_key.withheld"
-    )
+    val ROOM_KEY_WITHHELD = StableUnstableId(stable = "m.room_key.withheld", unstable = "org.matrix.room_key.withheld")
 
     const val REQUEST_SECRET = "m.secret.request"
     const val SEND_SECRET = "m.secret.send"
@@ -111,9 +106,9 @@ object EventType {
     const val REACTION = "m.reaction"
 
     // Poll
-    val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
-    val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
-    val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
+    val POLL_START = StableUnstableId(stable = "m.poll.start", unstable = "org.matrix.msc3381.poll.start")
+    val POLL_RESPONSE = StableUnstableId(stable = "m.poll.response", unstable = "org.matrix.msc3381.poll.response")
+    val POLL_END = StableUnstableId(stable = "m.poll.end", unstable = "org.matrix.msc3381.poll.end")
 
     // Unwedging
     internal const val DUMMY = "m.dummy"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
index 8f214e0f89..634e71c43b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
@@ -33,5 +33,7 @@ object RoomSummaryConstants {
             EventType.ENCRYPTED,
             EventType.STICKER,
             EventType.REACTION
-    ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+    ) +
+            EventType.POLL_START.values +
+            EventType.STATE_ROOM_BEACON_INFO.values
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 6b4a0226a0..9053425a39 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -147,9 +147,9 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
         // Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
         // so toModel won't parse them correctly
         // It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
-        in EventType.POLL_START -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
-        in EventType.STATE_ROOM_BEACON_INFO -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
-        in EventType.BEACON_LOCATION_DATA -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+        in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+        in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+        in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
         else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
index 6a4abe9d34..b6ad7581fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
@@ -41,9 +41,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
             EventType.ENCRYPTED,
-            EventType.CALL_ASSERTED_IDENTITY,
-            EventType.CALL_ASSERTED_IDENTITY_PREFIX
-    )
+    ) +
+            EventType.CALL_ASSERTED_IDENTITY.values
 
     private val eventsToPostProcess = mutableListOf()
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
index 48a9dfd3da..d824aaa51a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
@@ -84,8 +84,7 @@ internal class CallSignalingHandler @Inject constructor(
             EventType.CALL_NEGOTIATE -> {
                 handleCallNegotiateEvent(event)
             }
-            EventType.CALL_ASSERTED_IDENTITY,
-            EventType.CALL_ASSERTED_IDENTITY_PREFIX -> {
+            in EventType.CALL_ASSERTED_IDENTITY.values -> {
                 handleCallAssertedIdentityEvent(event)
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
index 09d7d50ecb..9fe93d8262 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
@@ -56,8 +56,8 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
 
         val allEvents = (newJoinEvents + inviteEvents).filter { event ->
             when (event.type) {
-                in EventType.POLL_START,
-                in EventType.STATE_ROOM_BEACON_INFO,
+                in EventType.POLL_START.values,
+                in EventType.STATE_ROOM_BEACON_INFO.values,
                 EventType.MESSAGE,
                 EventType.REDACTION,
                 EventType.ENCRYPTED,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 48e75821bd..be73309837 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -91,7 +91,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             EventType.KEY_VERIFICATION_READY,
             EventType.KEY_VERIFICATION_KEY,
             EventType.ENCRYPTED
-    ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
+    ) +
+            EventType.POLL_START.values +
+            EventType.POLL_RESPONSE.values +
+            EventType.POLL_END.values +
+            EventType.STATE_ROOM_BEACON_INFO.values +
+            EventType.BEACON_LOCATION_DATA.values
 
     override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
         return allowedTypes.contains(eventType)
@@ -208,7 +213,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         }
                     }
                 }
-                in EventType.POLL_START -> {
+                in EventType.POLL_START.values -> {
                     val content: MessagePollContent? = event.content.toModel()
                     if (content?.relatesTo?.type == RelationType.REPLACE) {
                         Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
@@ -216,26 +221,26 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         handleReplace(realm, event, roomId, isLocalEcho, content.relatesTo.eventId)
                     }
                 }
-                in EventType.POLL_RESPONSE -> {
+                in EventType.POLL_RESPONSE.values -> {
                     event.content.toModel(catchError = true)?.let {
                         sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
                             pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
                         }
                     }
                 }
-                in EventType.POLL_END -> {
+                in EventType.POLL_END.values -> {
                     sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
                         getPowerLevelsHelper(event.roomId)?.let {
                             pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
                         }
                     }
                 }
-                in EventType.STATE_ROOM_BEACON_INFO -> {
+                in EventType.STATE_ROOM_BEACON_INFO.values -> {
                     event.content.toModel(catchError = true)?.let {
                         liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho)
                     }
                 }
-                in EventType.BEACON_LOCATION_DATA -> {
+                in EventType.BEACON_LOCATION_DATA.values -> {
                     handleBeaconLocationData(event, realm, roomId, isLocalEcho)
                 }
                 else -> Timber.v("UnHandled event ${event.eventId}")
@@ -324,7 +329,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             }
         }
 
-        if (event.getClearType() in EventType.POLL_START) {
+        if (event.getClearType() in EventType.POLL_START.values) {
             pollAggregationProcessor.handlePollStartEvent(realm, event)
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
index a8d955af1d..ae7022a204 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
@@ -39,7 +39,7 @@ internal class DefaultGetActiveBeaconInfoForUserTask @Inject constructor(
 ) : GetActiveBeaconInfoForUserTask {
 
     override suspend fun execute(params: GetActiveBeaconInfoForUserTask.Params): Event? {
-        return EventType.STATE_ROOM_BEACON_INFO
+        return EventType.STATE_ROOM_BEACON_INFO.values
                 .mapNotNull {
                     stateEventDataSource.getStateEvent(
                             roomId = params.roomId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
index 9041ef2677..dbdc5dc228 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
@@ -48,7 +48,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() :
         val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst()
                 ?: return
 
-        if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) {
+        if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO.values) {
             val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId)
 
             if (liveSummary != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
index 781def1abe..13753115ac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
@@ -46,7 +46,7 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
                 isLive = true,
                 unstableTimestampMillis = clock.epochMillis()
         ).toContent()
-        val eventType = EventType.STATE_ROOM_BEACON_INFO.first()
+        val eventType = EventType.STATE_ROOM_BEACON_INFO.stable
         val sendStateTaskParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = userId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
index da5fd76940..40f7aa2dd2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
@@ -45,7 +45,7 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor(
         val sendStateTaskParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = stateKey,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 body = updatedContent
         )
         return try {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index 7968eabd30..3d1ac71bb4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -74,9 +74,9 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
             when (typeToPrune) {
                 EventType.ENCRYPTED,
                 EventType.MESSAGE,
-                in EventType.STATE_ROOM_BEACON_INFO,
-                in EventType.BEACON_LOCATION_DATA,
-                in EventType.POLL_START -> {
+                in EventType.STATE_ROOM_BEACON_INFO.values,
+                in EventType.BEACON_LOCATION_DATA.values,
+                in EventType.POLL_START.values -> {
                     Timber.d("REDACTION for message ${eventToPrune.eventId}")
                     val unsignedData = EventMapper.map(eventToPrune).unsignedData
                             ?: UnsignedData(null, null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 55ba78c2a5..2f8be69473 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -181,7 +181,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.first(),
+                type = EventType.POLL_START.stable,
                 content = newContent.toContent().plus(additionalContent.orEmpty())
         )
     }
@@ -206,7 +206,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_RESPONSE.first(),
+                type = EventType.POLL_RESPONSE.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -226,7 +226,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.first(),
+                type = EventType.POLL_START.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -249,7 +249,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_END.first(),
+                type = EventType.POLL_END.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -300,7 +300,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.BEACON_LOCATION_DATA.first(),
+                type = EventType.BEACON_LOCATION_DATA.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
index 129d49633e..bdd1fd9b0d 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
@@ -87,7 +87,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_START_EVENT = Event(
-            type = EventType.POLL_START.first(),
+            type = EventType.POLL_START.stable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
@@ -96,7 +96,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_RESPONSE_EVENT = Event(
-            type = EventType.POLL_RESPONSE.first(),
+            type = EventType.POLL_RESPONSE.stable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
@@ -105,7 +105,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_END_EVENT = Event(
-            type = EventType.POLL_END.first(),
+            type = EventType.POLL_END.stable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
index d51ed77399..4a10795647 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
@@ -69,7 +69,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest {
         result shouldBeEqualTo currentStateEvent
         fakeStateEventDataSource.verifyGetStateEvent(
                 roomId = params.roomId,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 stateKey = QueryStringValue.Equals(A_USER_ID)
         )
     }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
index fdc411cc9d..a5c126cf72 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
@@ -75,7 +75,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
         val expectedParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = A_USER_ID,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 body = expectedBeaconContent
         )
         fakeSendStateTask.verifyExecuteRetry(
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
index 1abf179ccf..a7adadfc63 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
@@ -79,7 +79,7 @@ class DefaultStopLiveLocationShareTaskTest {
         val expectedSendParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = A_USER_ID,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 body = expectedBeaconContent
         )
         fakeSendStateTask.verifyExecuteRetry(
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
index 24d9c30039..d6edb69d93 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
@@ -79,7 +79,7 @@ class LiveLocationShareRedactionEventProcessorTest {
     @Test
     fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest {
         val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID)
-        val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first())
+        val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.stable)
         fakeRealm.givenWhere()
                 .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
                 .givenFindFirst(redactedEventEntity)
diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
index d907f39ee3..63144ca1b3 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
@@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
 
 fun TimelineEvent.canReact(): Boolean {
     // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
-    return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START &&
+    return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values &&
             root.sendState == SendState.SYNCED &&
             !root.isRedacted()
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 9bed0aae04..34d7e45028 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -1774,7 +1774,7 @@ class TimelineFragment :
                 timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
             }
             is EventSharedAction.Edit -> {
-                if (action.eventType in EventType.POLL_START) {
+                if (action.eventType in EventType.POLL_START.values) {
                     navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT)
                 } else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
                     messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId))
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt
index 8cb82691d9..dedd4a53c8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt
@@ -33,8 +33,8 @@ class CheckIfCanRedactEventUseCase @Inject constructor(
                 EventType.STICKER,
                 VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
         ) +
-                EventType.POLL_START +
-                EventType.STATE_ROOM_BEACON_INFO
+                EventType.POLL_START.values +
+                EventType.STATE_ROOM_BEACON_INFO.values
 
         return event.root.getClearType() in canRedactEventTypes &&
                 // Message sent by the current user can always be redacted, else check permission for messages sent by other users
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
index c312ef31b7..a9df059cc1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
@@ -26,7 +26,7 @@ class CheckIfCanReplyEventUseCase @Inject constructor() {
 
     fun execute(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
         // Only EventType.MESSAGE, EventType.POLL_START and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment
-        if (event.root.getClearType() !in EventType.STATE_ROOM_BEACON_INFO + EventType.POLL_START + EventType.MESSAGE) return false
+        if (event.root.getClearType() !in EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE) return false
         if (!actionPermissions.canSendMessage) return false
         return when (messageContent?.msgType) {
             MessageType.MSGTYPE_TEXT,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 0c44ee386d..a6d7e8386f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -215,7 +215,7 @@ class MessageActionsViewModel @AssistedInject constructor(
                     EventType.CALL_ANSWER -> {
                         noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
                     }
-                    in EventType.POLL_START -> {
+                    in EventType.POLL_START.values -> {
                         timelineEvent.root.getClearContent().toModel(catchError = true)
                                 ?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
                     }
@@ -383,7 +383,7 @@ class MessageActionsViewModel @AssistedInject constructor(
             }
 
             if (canRedact(timelineEvent, actionPermissions)) {
-                if (timelineEvent.root.getClearType() in EventType.POLL_START) {
+                if (timelineEvent.root.getClearType() in EventType.POLL_START.values) {
                     add(
                             EventSharedAction.Redact(
                                     eventId,
@@ -530,13 +530,13 @@ class MessageActionsViewModel @AssistedInject constructor(
 
     private fun canViewReactions(event: TimelineEvent): Boolean {
         // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
-        if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false
+        if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values) return false
         return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
     }
 
     private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean {
         // Only event of type EventType.MESSAGE and EventType.POLL_START are supported for the moment
-        if (event.root.getClearType() !in listOf(EventType.MESSAGE) + EventType.POLL_START) return false
+        if (event.root.getClearType() !in listOf(EventType.MESSAGE) + EventType.POLL_START.values) return false
         if (!actionPermissions.canSendMessage) return false
         // TODO if user is admin or moderator
         val messageContent = event.root.getClearContent().toModel()
@@ -582,14 +582,14 @@ class MessageActionsViewModel @AssistedInject constructor(
     }
 
     private fun canEndPoll(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
-        return event.root.getClearType() in EventType.POLL_START &&
+        return event.root.getClearType() in EventType.POLL_START.values &&
                 canRedact(event, actionPermissions) &&
                 event.annotations?.pollResponseSummary?.closedTime == null
     }
 
     private fun canEditPoll(event: TimelineEvent): Boolean {
-        return event.root.getClearType() in EventType.POLL_START &&
+        return event.root.getClearType() in EventType.POLL_START.values &&
                 event.annotations?.pollResponseSummary?.closedTime == null &&
-                event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0
+                (event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0) == 0
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 31ff257214..ae3ea143a7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -88,7 +88,7 @@ class TimelineItemFactory @Inject constructor(
                     EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
                     // State room create
                     EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
-                    in EventType.STATE_ROOM_BEACON_INFO -> messageItemFactory.create(params)
+                    in EventType.STATE_ROOM_BEACON_INFO.values -> messageItemFactory.create(params)
                     VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> messageItemFactory.create(params)
                     // Unhandled state event types
                     else -> {
@@ -101,7 +101,7 @@ class TimelineItemFactory @Inject constructor(
                 when (event.root.getClearType()) {
                     // Message itemsX
                     EventType.STICKER,
-                    in EventType.POLL_START,
+                    in EventType.POLL_START.values,
                     EventType.MESSAGE -> messageItemFactory.create(params)
                     EventType.REDACTION,
                     EventType.KEY_VERIFICATION_ACCEPT,
@@ -114,9 +114,9 @@ class TimelineItemFactory @Inject constructor(
                     EventType.CALL_SELECT_ANSWER,
                     EventType.CALL_NEGOTIATE,
                     EventType.REACTION,
-                    in EventType.POLL_RESPONSE,
-                    in EventType.POLL_END -> noticeItemFactory.create(params)
-                    in EventType.BEACON_LOCATION_DATA -> {
+                    in EventType.POLL_RESPONSE.values,
+                    in EventType.POLL_END.values -> noticeItemFactory.create(params)
+                    in EventType.BEACON_LOCATION_DATA.values -> {
                         if (event.root.isRedacted()) {
                             messageItemFactory.create(params)
                         } else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index eb531b6f1b..aaa0fc10c9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -122,17 +122,17 @@ class DisplayableEventFormatter @Inject constructor(
             EventType.CALL_CANDIDATES -> {
                 span { }
             }
-            in EventType.POLL_START -> {
+            in EventType.POLL_START.values -> {
                 timelineEvent.root.getClearContent().toModel(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
                         ?: stringProvider.getString(R.string.sent_a_poll)
             }
-            in EventType.POLL_RESPONSE -> {
+            in EventType.POLL_RESPONSE.values -> {
                 stringProvider.getString(R.string.poll_response_room_list_preview)
             }
-            in EventType.POLL_END -> {
+            in EventType.POLL_END.values -> {
                 stringProvider.getString(R.string.poll_end_room_list_preview)
             }
-            in EventType.STATE_ROOM_BEACON_INFO -> {
+            in EventType.STATE_ROOM_BEACON_INFO.values -> {
                 simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor)
             }
             else -> {
@@ -220,17 +220,17 @@ class DisplayableEventFormatter @Inject constructor(
                     emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key))
                 } ?: span { }
             }
-            in EventType.POLL_START -> {
+            in EventType.POLL_START.values -> {
                 event.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question
                         ?: stringProvider.getString(R.string.sent_a_poll)
             }
-            in EventType.POLL_RESPONSE -> {
+            in EventType.POLL_RESPONSE.values -> {
                 stringProvider.getString(R.string.poll_response_room_list_preview)
             }
-            in EventType.POLL_END -> {
+            in EventType.POLL_END.values -> {
                 stringProvider.getString(R.string.poll_end_room_list_preview)
             }
-            in EventType.STATE_ROOM_BEACON_INFO -> {
+            in EventType.STATE_ROOM_BEACON_INFO.values -> {
                 stringProvider.getString(R.string.sent_live_location)
             }
             else -> {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index def3fb1a44..3f702ed72d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -107,9 +107,9 @@ class NoticeEventFormatter @Inject constructor(
             EventType.STATE_SPACE_PARENT,
             EventType.REDACTION,
             EventType.STICKER,
-            in EventType.POLL_RESPONSE,
-            in EventType.POLL_END,
-            in EventType.BEACON_LOCATION_DATA,
+            in EventType.POLL_RESPONSE.values,
+            in EventType.POLL_END.values,
+            in EventType.BEACON_LOCATION_DATA.values,
             VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatDebug(timelineEvent.root)
             else -> {
                 Timber.v("Type $type not handled by this formatter")
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index 2411cb3877..51e961f247 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -54,9 +54,9 @@ object TimelineDisplayableEvents {
             EventType.KEY_VERIFICATION_CANCEL,
             VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
     ) +
-            EventType.POLL_START +
-            EventType.STATE_ROOM_BEACON_INFO +
-            EventType.BEACON_LOCATION_DATA
+            EventType.POLL_START.values +
+            EventType.STATE_ROOM_BEACON_INFO.values +
+            EventType.BEACON_LOCATION_DATA.values
 }
 
 fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index cce20d102c..1360151074 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -248,7 +248,7 @@ class TimelineEventVisibilityHelper @Inject constructor(
             } else root.eventId != rootThreadEventId
         }
 
-        if (root.getClearType() in EventType.BEACON_LOCATION_DATA) {
+        if (root.getClearType() in EventType.BEACON_LOCATION_DATA.values) {
             return !root.isRedacted()
         }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
index 379e5b3b91..c207a5f67e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
@@ -47,8 +47,10 @@ class TimelineMessageLayoutFactory @Inject constructor(
         private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf(
                 EventType.MESSAGE,
                 EventType.ENCRYPTED,
-                EventType.STICKER
-        ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+                EventType.STICKER,
+        ) +
+                EventType.POLL_START.values +
+                EventType.STATE_ROOM_BEACON_INFO.values
 
         // Can't be rendered in bubbles, so get back to default layout
         private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf(
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
index 4c7abd99b8..cdef7d3302 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
@@ -83,7 +83,7 @@ class LocationSharingViewModel @AssistedInject constructor(
                 .distinctUntilChanged()
                 .setOnEach {
                     val powerLevelsHelper = PowerLevelsHelper(it)
-                    val canShareLiveLocation = EventType.STATE_ROOM_BEACON_INFO
+                    val canShareLiveLocation = EventType.STATE_ROOM_BEACON_INFO.values
                             .all { beaconInfoType ->
                                 powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, beaconInfoType)
                             }
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
index ba1d5c7f6f..d28ab22684 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
@@ -66,7 +66,7 @@ class NotifiableEventResolver @Inject constructor(
 ) {
 
     private val nonEncryptedNotifiableEventTypes: List =
-            listOf(EventType.MESSAGE) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+            listOf(EventType.MESSAGE) + EventType.POLL_START.values + EventType.STATE_ROOM_BEACON_INFO.values
 
     suspend fun resolveEvent(event: Event, session: Session, isNoisy: Boolean): NotifiableEvent? {
         val roomID = event.roomId ?: return null
diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt
index e2157c3af0..bedb289a39 100644
--- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt
@@ -35,8 +35,13 @@ class CheckIfCanRedactEventUseCaseTest {
 
     @Test
     fun `given an event which can be redacted and owned by user when use case executes then the result is true`() {
-        val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) +
-                EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+        val canRedactEventTypes = listOf(
+                EventType.MESSAGE,
+                EventType.STICKER,
+                VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
+        ) +
+                EventType.POLL_START.values +
+                EventType.STATE_ROOM_BEACON_INFO.values
 
         canRedactEventTypes.forEach { eventType ->
             val event = givenAnEvent(
diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt
index 83d23681fc..51082e0e06 100644
--- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCaseTest.kt
@@ -43,7 +43,7 @@ class CheckIfCanReplyEventUseCaseTest {
 
     @Test
     fun `given reply is allowed for the event type when use case is executed then result is true`() {
-        val eventTypes = EventType.STATE_ROOM_BEACON_INFO + EventType.POLL_START + EventType.MESSAGE
+        val eventTypes = EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE
 
         eventTypes.forEach { eventType ->
             val event = givenAnEvent(eventType)
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt
index 04f3526602..42a500671b 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt
@@ -63,7 +63,7 @@ object FakeCreatePollViewStates {
     )
 
     private val A_POLL_START_EVENT = Event(
-            type = EventType.POLL_START.first(),
+            type = EventType.POLL_START.stable,
             eventId = A_FAKE_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_FAKE_USER_ID,
@@ -80,8 +80,8 @@ object FakeCreatePollViewStates {
     )
 
     val initialCreatePollViewState = CreatePollViewState(createPollArgs).copy(
-        canCreatePoll = false,
-        canAddMoreOptions = true
+            canCreatePoll = false,
+            canAddMoreOptions = true
     )
 
     val pollViewStateWithOnlyQuestion = initialCreatePollViewState.copy(

From 0209cc4969a34b48b651784ec193fb2e4db56fb4 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 17 Nov 2022 11:54:00 +0100
Subject: [PATCH 345/679] Prune redacted events which are not explicitly
 restricted

---
 .../room/prune/RedactionEventProcessor.kt     | 78 ++++++++++++-------
 1 file changed, 48 insertions(+), 30 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index 3d1ac71bb4..9de55968f1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -61,45 +61,33 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
         val isLocalEcho = LocalEcho.isLocalEchoId(redactionEvent.eventId ?: "")
         Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
 
-        val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
-                ?: return
+        val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return
 
         val typeToPrune = eventToPrune.type
         val stateKey = eventToPrune.stateKey
         val allowedKeys = computeAllowedKeys(typeToPrune)
-        if (allowedKeys.isNotEmpty()) {
-            val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
-            eventToPrune.content = ContentMapper.map(prunedContent)
-        } else {
-            when (typeToPrune) {
-                EventType.ENCRYPTED,
-                EventType.MESSAGE,
-                in EventType.STATE_ROOM_BEACON_INFO.values,
-                in EventType.BEACON_LOCATION_DATA.values,
-                in EventType.POLL_START.values -> {
-                    Timber.d("REDACTION for message ${eventToPrune.eventId}")
-                    val unsignedData = EventMapper.map(eventToPrune).unsignedData
-                            ?: UnsignedData(null, null)
-
-                    // was this event a m.replace
+        when {
+            allowedKeys.isNotEmpty() -> {
+                val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
+                eventToPrune.content = ContentMapper.map(prunedContent)
+            }
+            canPruneEventType(typeToPrune) -> {
+                Timber.d("REDACTION for message ${eventToPrune.eventId}")
+                val unsignedData = EventMapper.map(eventToPrune).unsignedData ?: UnsignedData(null, null)
+
+                // was this event a m.replace
 //                    val contentModel = ContentMapper.map(eventToPrune.content)?.toModel()
 //                    if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
 //                        eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
 //                    }
 
-                    val modified = unsignedData.copy(redactedEvent = redactionEvent)
-                    // Deleting the content of a thread message will result to delete the thread relation, however threads are now dynamic
-                    // so there is not much of a problem
-                    eventToPrune.content = ContentMapper.map(emptyMap())
-                    eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
-                    eventToPrune.decryptionResultJson = null
-                    eventToPrune.decryptionErrorCode = null
-
-                    handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
-                }
-//                EventType.REACTION -> {
-//                    eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
-//                }
+                val modified = unsignedData.copy(redactedEvent = redactionEvent)
+                eventToPrune.content = ContentMapper.map(emptyMap())
+                eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
+                eventToPrune.decryptionResultJson = null
+                eventToPrune.decryptionErrorCode = null
+
+                handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
             }
         }
         if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
@@ -167,4 +155,34 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
             else -> emptyList()
         }
     }
+
+    private fun canPruneEventType(eventType: String): Boolean {
+        return when {
+            EventType.isCallEvent(eventType) -> false
+            EventType.isVerificationEvent(eventType) -> false
+            eventType == EventType.ROOM_KEY ||
+                    eventType == EventType.STATE_ROOM_WIDGET_LEGACY ||
+                    eventType == EventType.STATE_ROOM_WIDGET ||
+                    eventType == EventType.STATE_ROOM_NAME ||
+                    eventType == EventType.STATE_ROOM_TOPIC ||
+                    eventType == EventType.STATE_ROOM_AVATAR ||
+                    eventType == EventType.STATE_ROOM_THIRD_PARTY_INVITE ||
+                    eventType == EventType.STATE_ROOM_GUEST_ACCESS ||
+                    eventType == EventType.STATE_SPACE_CHILD ||
+                    eventType == EventType.STATE_SPACE_PARENT ||
+                    eventType == EventType.STATE_ROOM_TOMBSTONE ||
+                    eventType == EventType.STATE_ROOM_HISTORY_VISIBILITY ||
+                    eventType == EventType.STATE_ROOM_RELATED_GROUPS ||
+                    eventType == EventType.STATE_ROOM_PINNED_EVENT ||
+                    eventType == EventType.STATE_ROOM_ENCRYPTION ||
+                    eventType == EventType.STATE_ROOM_SERVER_ACL ||
+                    eventType == EventType.ROOM_KEY_REQUEST ||
+                    eventType == EventType.FORWARDED_ROOM_KEY ||
+                    eventType in EventType.ROOM_KEY_WITHHELD.values ||
+                    eventType == EventType.REQUEST_SECRET ||
+                    eventType == EventType.SEND_SECRET ||
+                    eventType == EventType.REACTION -> false
+            else -> true
+        }
+    }
 }

From 2477632e2bb8cd5e81b5386b23071049d2df26a3 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 23 Nov 2022 13:58:31 +0100
Subject: [PATCH 346/679] Fix exception when getting models on some redacted
 event

An exception was triggered because the excepted model body was null for redacted events
---
 .../android/sdk/api/session/events/model/Event.kt    | 12 ++----------
 1 file changed, 2 insertions(+), 10 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index f84e9858a5..40ce6ecb5c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -26,10 +26,8 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
@@ -400,14 +398,8 @@ fun Event.getRelationContent(): RelationDefaultContent? {
     return if (isEncrypted()) {
         content.toModel()?.relatesTo
     } else {
-        content.toModel()?.relatesTo ?: run {
-            // Special cases when there is only a local msgtype for some event types
-            when (getClearType()) {
-                EventType.STICKER -> getClearContent().toModel()?.relatesTo
-                in EventType.BEACON_LOCATION_DATA.values -> getClearContent().toModel()?.relatesTo
-                else -> getClearContent()?.get("m.relates_to")?.toContent().toModel()
-            }
-        }
+        content.toModel()?.relatesTo
+                ?: getClearContent()?.get("m.relates_to")?.toContent().toModel() // Special cases when there is only a local msgtype for some event types
     }
 }
 

From 5a43b764888697876aa9a80645fa5cf77912b28d Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 25 Nov 2022 10:00:05 +0100
Subject: [PATCH 347/679] Log a warning if the event content is not pruned

---
 .../sdk/internal/session/room/prune/RedactionEventProcessor.kt | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index 9de55968f1..f9b0e893cb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -89,6 +89,9 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
 
                 handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
             }
+            else -> {
+                Timber.w("Not pruning event (type $typeToPrune)")
+            }
         }
         if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
             TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId).forEach {

From a8f3bb1d4e294edb6bb11218efb69d77f9b3780a Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 25 Nov 2022 10:16:35 +0100
Subject: [PATCH 348/679] Remove to-device events from event type filtering for
 redaction

---
 .../session/room/prune/RedactionEventProcessor.kt         | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index f9b0e893cb..0cff2c5a7c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -163,8 +163,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
         return when {
             EventType.isCallEvent(eventType) -> false
             EventType.isVerificationEvent(eventType) -> false
-            eventType == EventType.ROOM_KEY ||
-                    eventType == EventType.STATE_ROOM_WIDGET_LEGACY ||
+            eventType == EventType.STATE_ROOM_WIDGET_LEGACY ||
                     eventType == EventType.STATE_ROOM_WIDGET ||
                     eventType == EventType.STATE_ROOM_NAME ||
                     eventType == EventType.STATE_ROOM_TOPIC ||
@@ -179,11 +178,6 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
                     eventType == EventType.STATE_ROOM_PINNED_EVENT ||
                     eventType == EventType.STATE_ROOM_ENCRYPTION ||
                     eventType == EventType.STATE_ROOM_SERVER_ACL ||
-                    eventType == EventType.ROOM_KEY_REQUEST ||
-                    eventType == EventType.FORWARDED_ROOM_KEY ||
-                    eventType in EventType.ROOM_KEY_WITHHELD.values ||
-                    eventType == EventType.REQUEST_SECRET ||
-                    eventType == EventType.SEND_SECRET ||
                     eventType == EventType.REACTION -> false
             else -> true
         }

From af59a581572afa9a6c838be30bd6e00621adff9a Mon Sep 17 00:00:00 2001
From: Platon Terekhov 
Date: Fri, 25 Nov 2022 20:09:59 +0000
Subject: [PATCH 349/679] Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/
---
 .../ui-strings/src/main/res/values-uk/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml
index 27511255ee..9bf99c1e99 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -2965,4 +2965,17 @@
     
     Вийти
     Залишилося %1$s
+    відправив аудіофайл.
+    відправив файл.
+    У відповідь на
+    Сховати IP-адресу
+    створив голосування.
+    відправив наліпку.
+    відправив відео.
+    відправив зображення.
+    відправив голосове повідомлення.
+    Показати IP-адресу
+    Цитуючи
+    У відповідь на %s
+    Редагування
 
\ No newline at end of file

From 41a20bf4d08fee44d24275dd93b5d22c20a6d639 Mon Sep 17 00:00:00 2001
From: Christina Klaas 
Date: Fri, 25 Nov 2022 11:08:37 +0000
Subject: [PATCH 350/679] Translated using Weblate (German)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
---
 library/ui-strings/src/main/res/values-de/strings.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml
index 75d77ea718..be53c15026 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -2333,9 +2333,9 @@
     ${app_name} konnte nicht auf deinen Standort zugreifen
     Standort
     Die Ergebnisse werden erst sichtbar, sobald du die Umfrage beendest
-    Abgeschlossene Umfrage
+    Versteckte Umfrage
     Abstimmende können die Ergebnisse nach Stimmabgabe sehen
-    Laufende Umfrage
+    Offene Umfrage
     Umfragetyp
     Umfrage bearbeiten
     Keine Stimmen abgegeben

From b7c8ae7c450644c26be39ba5f457bf7638596110 Mon Sep 17 00:00:00 2001
From: Glandos 
Date: Fri, 25 Nov 2022 11:49:50 +0000
Subject: [PATCH 351/679] Translated using Weblate (French)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/
---
 .../ui-strings/src/main/res/values-fr/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml
index ca462c592f..fae32dc52e 100644
--- a/library/ui-strings/src/main/res/values-fr/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr/strings.xml
@@ -2853,4 +2853,17 @@
     
     Déconnecter
     %1$s restant
+    a créé un sondage.
+    a envoyé un autocollant.
+    a envoyé une vidéo.
+    a envoyé une image.
+    envoyer un message vocal.
+    a envoyé un fichier audio.
+    a envoyé un fichier.
+    En réponse à
+    Masquer l’adresse IP
+    Afficher l’adresse IP
+    Citation de
+    Réponse à %s
+    Modification
 
\ No newline at end of file

From e925a2c66929a16bb6497a4ae39a00a99ae4ad4c Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Fri, 25 Nov 2022 11:40:12 +0000
Subject: [PATCH 352/679] Translated using Weblate (French)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/
---
 library/ui-strings/src/main/res/values-fr/strings.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml
index fae32dc52e..cf49733bdf 100644
--- a/library/ui-strings/src/main/res/values-fr/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr/strings.xml
@@ -2846,7 +2846,7 @@
     Retour rapide de 30 secondes
     Les sessions vérifiées sont toutes celles qui utilisent ce compte après avoir saisie la phrase de sécurité ou confirmé votre identité à l’aide d’une autre session vérifiée.
 \n
-\nCela veut dire qu’elles disposent de toutes les clés nécessaires pour lire les messages chiffrés, et confirment aux autres utilisateur que vous faites confiance à cette session.
+\nCela veut dire qu’elles disposent de toutes les clés nécessaires pour lire les messages chiffrés, et confirment aux autres utilisateurs que vous faites confiance à cette session.
     
         Déconnecter %1$d session
         Déconnecter %1$d sessions

From 02deb1bf191cb08d4a4443f2071cdd18d455924a Mon Sep 17 00:00:00 2001
From: Szimszon 
Date: Fri, 25 Nov 2022 15:09:03 +0000
Subject: [PATCH 353/679] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/hu/
---
 .../ui-strings/src/main/res/values-hu/strings.xml  | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml
index 8f449dadaf..1dd2134b90 100644
--- a/library/ui-strings/src/main/res/values-hu/strings.xml
+++ b/library/ui-strings/src/main/res/values-hu/strings.xml
@@ -2852,4 +2852,18 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
     Az új hang közvetítés nem indítható el
     30 másodperccel előre
     30 másodperccel vissza
+    visszavan: %1$s
+    szavazás elkészítve.
+    matrica elküldve.
+    videót küldött.
+    kép elküldve.
+    hang üzenet elküldve.
+    hangfájl elküldve.
+    fájl elküldve.
+    Válaszolva erre
+    IP címek elrejtése
+    IP címek megjelenítése
+    Idézet
+    Válasz erre: %s
+    Szerkesztés
 
\ No newline at end of file

From 225bf09251614254e76ee357ec90ff0fb132ebed Mon Sep 17 00:00:00 2001
From: Platon Terekhov 
Date: Fri, 25 Nov 2022 20:05:18 +0000
Subject: [PATCH 354/679] Translated using Weblate (Russian)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/
---
 .../ui-strings/src/main/res/values-ru/strings.xml   | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml
index 8cc7bc54e0..39d1c8de2b 100644
--- a/library/ui-strings/src/main/res/values-ru/strings.xml
+++ b/library/ui-strings/src/main/res/values-ru/strings.xml
@@ -2950,4 +2950,17 @@
     Дать разрешение
     Другие пользователи могут найти вас по %s
     Осталось %1$s
+    создал опрос.
+    отправил наклейку.
+    отправил видео.
+    отправил изображение.
+    отправил голосовое сообщение.
+    отправил аудиофайл.
+    отправил файл.
+    В ответ на
+    Скрыть IP-адрес
+    Показать IP-адрес
+    Цитируя
+    В ответ на %s
+    Редактирование
 
\ No newline at end of file

From 40f0f59db159cb026f3b9cc9e791ebf90d15f9e2 Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk 
Date: Fri, 25 Nov 2022 20:50:47 +0000
Subject: [PATCH 355/679] Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/
---
 library/ui-strings/src/main/res/values-uk/strings.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml
index 9bf99c1e99..19889892ff 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -2965,7 +2965,7 @@
     
     Вийти
     Залишилося %1$s
-    відправив аудіофайл.
+    надсилає аудіофайл.
     відправив файл.
     У відповідь на
     Сховати IP-адресу

From 6cbd39f3d3a4642b840d8840ea952858df9808f3 Mon Sep 17 00:00:00 2001
From: Glandos 
Date: Fri, 25 Nov 2022 11:48:09 +0000
Subject: [PATCH 356/679] Translated using Weblate (French)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/fr/
---
 fastlane/metadata/android/fr-FR/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/fr-FR/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/fr-FR/changelogs/40105080.txt b/fastlane/metadata/android/fr-FR/changelogs/40105080.txt
new file mode 100644
index 0000000000..d33197c270
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Principaux changements pour cette version : corrections de bugs et améliorations.
+Intégralité des changements : https://github.com/vector-im/element-android/releases

From e28f0d0713f2cf02ae09c837c577b0b06806d2de Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 25 Nov 2022 23:03:08 +0000
Subject: [PATCH 357/679] Bump libphonenumber from 8.13.0 to 8.13.1

Bumps [libphonenumber](https://github.com/google/libphonenumber) from 8.13.0 to 8.13.1.
- [Release notes](https://github.com/google/libphonenumber/releases)
- [Changelog](https://github.com/google/libphonenumber/blob/master/making-metadata-changes.md)
- [Commits](https://github.com/google/libphonenumber/compare/v8.13.0...v8.13.1)

---
updated-dependencies:
- dependency-name: com.googlecode.libphonenumber:libphonenumber
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index 93098952e6..31c32bb26b 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -83,7 +83,7 @@ ext.libs = [
                 'appdistributionApi'      : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
                 'appdistribution'         : "com.google.firebase:firebase-appdistribution:$appDistribution",
                 // Phone number https://github.com/google/libphonenumber
-                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.13.0"
+                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.13.1"
         ],
         dagger      : [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",

From d7dc5c812e3e7132bfeb34fe090e9f80b6bc89ed Mon Sep 17 00:00:00 2001
From: gradle-update-robot 
Date: Sun, 27 Nov 2022 00:26:10 +0000
Subject: [PATCH 358/679] Update Gradle Wrapper from 7.5.1 to 7.6.

Signed-off-by: gradle-update-robot 
---
 gradle/wrapper/gradle-wrapper.jar        | Bin 60756 -> 61574 bytes
 gradle/wrapper/gradle-wrapper.properties |   5 +++--
 gradlew                                  |  12 ++++++++----
 gradlew.bat                              |   1 +
 4 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 249e5832f090a2944b7473328c07c9755baa3196..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644
GIT binary patch
delta 36524
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%0DGK?7GHTj
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
VwlMN}`%er%aGR6olj~j${vQ;P=LY})

diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f7189a776c..bc073f6761 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
+distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
+networkTimeout=10000
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index a69d9cb6c2..65dcd68d65 100755
--- a/gradlew
+++ b/gradlew
@@ -55,7 +55,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,10 +80,10 @@ do
     esac
 done
 
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@@ -143,12 +143,16 @@ fi
 if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
     case $MAX_FD in  #(
       '' | soft) :;; #(
       *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045 
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
diff --git a/gradlew.bat b/gradlew.bat
index 53a6b238d4..6689b85bee 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
 
 set DIRNAME=%~dp0
 if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 

From 5aeca1f81a22abf96af0a96574e05029a31df545 Mon Sep 17 00:00:00 2001
From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com>
Date: Mon, 28 Nov 2022 09:48:28 +0100
Subject: [PATCH 359/679] saving sync filter changed (#7627)

---
 changelog.d/7626.sdk                          |   2 +
 .../android/sdk/common/CommonTestHelper.kt    |   5 +
 .../sdk/api/session/sync/FilterService.kt     |  13 +-
 .../session/sync/filter/SyncFilterBuilder.kt  | 129 ++++++++++++++++++
 .../database/RealmSessionStoreMigration.kt    |   4 +-
 .../database/mapper/FilterParamsMapper.kt     |  61 +++++++++
 .../database/migration/MigrateSessionTo043.kt |   1 -
 .../database/migration/MigrateSessionTo045.kt |  38 ++++++
 .../database/model/SessionRealmModule.kt      |   3 +-
 .../database/model/SyncFilterParamsEntity.kt  |  36 +++++
 .../session/filter/DefaultFilterRepository.kt |  83 ++++++-----
 .../session/filter/DefaultFilterService.kt    |  22 ++-
 .../internal/session/filter/FilterFactory.kt  |  39 ------
 .../internal/session/filter/FilterModule.kt   |   3 +
 .../session/filter/FilterRepository.kt        |  31 ++++-
 .../session/filter/GetCurrentFilterTask.kt    |  55 ++++++++
 .../internal/session/filter/SaveFilterTask.kt |  43 ++----
 .../timeline/FetchTokenAndPaginateTask.kt     |   2 +-
 .../room/timeline/GetContextOfEventTask.kt    |   2 +-
 .../session/room/timeline/PaginationTask.kt   |   2 +-
 .../sdk/internal/session/sync/SyncTask.kt     |  15 +-
 .../internal/sync/filter/SyncFilterParams.kt  |  25 ++++
 .../sync/DefaultGetCurrentFilterTaskTest.kt   | 100 ++++++++++++++
 .../sdk/test/fakes/FakeFilterRepository.kt    |  34 +++++
 .../FakeHomeServerCapabilitiesDataSource.kt   |  30 ++++
 .../sdk/test/fakes/FakeSaveFilterTask.kt      |  40 ++++++
 .../ConfigureAndStartSessionUseCase.kt        |   6 +-
 .../im/vector/app/features/sync/SyncUtils.kt  |  48 +++++++
 .../ConfigureAndStartSessionUseCaseTest.kt    |   8 +-
 .../app/test/fakes/FakeFilterService.kt       |  11 +-
 30 files changed, 734 insertions(+), 157 deletions(-)
 create mode 100644 changelog.d/7626.sdk
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/sync/SyncUtils.kt

diff --git a/changelog.d/7626.sdk b/changelog.d/7626.sdk
new file mode 100644
index 0000000000..4d9f28183a
--- /dev/null
+++ b/changelog.d/7626.sdk
@@ -0,0 +1,2 @@
+Sync Filter now taking in account homeserver capabilities to not pass unsupported parameters.
+Sync Filter is now configured by providing SyncFilterBuilder class instance, instead of Filter to identify Filter changes related to homeserver capabilities
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index eeb2def582..8edecb273d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -50,6 +50,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
 import timber.log.Timber
 import java.util.UUID
 import java.util.concurrent.CountDownLatch
@@ -346,6 +347,10 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         assertTrue(registrationResult is RegistrationResult.Success)
         val session = (registrationResult as RegistrationResult.Success).session
         session.open()
+        session.filterService().setSyncFilter(
+                SyncFilterBuilder()
+                        .lazyLoadMembersForStateEvents(true)
+        )
         if (sessionTestParams.withInitialSync) {
             syncSession(session, 120_000)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
index bc592df474..7347bee165 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
@@ -16,19 +16,12 @@
 
 package org.matrix.android.sdk.api.session.sync
 
-interface FilterService {
-
-    enum class FilterPreset {
-        NoFilter,
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
 
-        /**
-         * Filter for Element, will include only known event type.
-         */
-        ElementFilter
-    }
+interface FilterService {
 
     /**
      * Configure the filter for the sync.
      */
-    fun setFilter(filterPreset: FilterPreset)
+    suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt
new file mode 100644
index 0000000000..ad55b26dfd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.sync.filter
+
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.internal.session.filter.Filter
+import org.matrix.android.sdk.internal.session.filter.RoomEventFilter
+import org.matrix.android.sdk.internal.session.filter.RoomFilter
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+class SyncFilterBuilder {
+    private var lazyLoadMembersForStateEvents: Boolean? = null
+    private var lazyLoadMembersForMessageEvents: Boolean? = null
+    private var useThreadNotifications: Boolean? = null
+    private var listOfSupportedEventTypes: List? = null
+    private var listOfSupportedStateEventTypes: List? = null
+
+    fun lazyLoadMembersForStateEvents(lazyLoadMembersForStateEvents: Boolean) = apply { this.lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents }
+
+    fun lazyLoadMembersForMessageEvents(lazyLoadMembersForMessageEvents: Boolean) =
+            apply { this.lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents }
+
+    fun useThreadNotifications(useThreadNotifications: Boolean) =
+            apply { this.useThreadNotifications = useThreadNotifications }
+
+    fun listOfSupportedStateEventTypes(listOfSupportedStateEventTypes: List) =
+            apply { this.listOfSupportedStateEventTypes = listOfSupportedStateEventTypes }
+
+    fun listOfSupportedTimelineEventTypes(listOfSupportedEventTypes: List) =
+            apply { this.listOfSupportedEventTypes = listOfSupportedEventTypes }
+
+    internal fun with(currentFilterParams: SyncFilterParams?) =
+            apply {
+                currentFilterParams?.let {
+                    useThreadNotifications = currentFilterParams.useThreadNotifications
+                    lazyLoadMembersForMessageEvents = currentFilterParams.lazyLoadMembersForMessageEvents
+                    lazyLoadMembersForStateEvents = currentFilterParams.lazyLoadMembersForStateEvents
+                    listOfSupportedEventTypes = currentFilterParams.listOfSupportedEventTypes?.toList()
+                    listOfSupportedStateEventTypes = currentFilterParams.listOfSupportedStateEventTypes?.toList()
+                }
+            }
+
+    internal fun extractParams(): SyncFilterParams {
+        return SyncFilterParams(
+                useThreadNotifications = useThreadNotifications,
+                lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = listOfSupportedEventTypes,
+                listOfSupportedStateEventTypes = listOfSupportedStateEventTypes,
+        )
+    }
+
+    internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter {
+        return Filter(
+                room = buildRoomFilter(homeServerCapabilities)
+        )
+    }
+
+    private fun buildRoomFilter(homeServerCapabilities: HomeServerCapabilities): RoomFilter {
+        return RoomFilter(
+                timeline = buildTimelineFilter(homeServerCapabilities),
+                state = buildStateFilter()
+        )
+    }
+
+    private fun buildTimelineFilter(homeServerCapabilities: HomeServerCapabilities): RoomEventFilter? {
+        val resolvedUseThreadNotifications = if (homeServerCapabilities.canUseThreadReadReceiptsAndNotifications) {
+            useThreadNotifications
+        } else {
+            null
+        }
+        return RoomEventFilter(
+                enableUnreadThreadNotifications = resolvedUseThreadNotifications,
+                lazyLoadMembers = lazyLoadMembersForMessageEvents
+        ).orNullIfEmpty()
+    }
+
+    private fun buildStateFilter(): RoomEventFilter? =
+            RoomEventFilter(
+                    lazyLoadMembers = lazyLoadMembersForStateEvents,
+                    types = listOfSupportedStateEventTypes
+            ).orNullIfEmpty()
+
+    private fun RoomEventFilter.orNullIfEmpty(): RoomEventFilter? {
+        return if (hasData()) {
+            this
+        } else {
+            null
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as SyncFilterBuilder
+
+        if (lazyLoadMembersForStateEvents != other.lazyLoadMembersForStateEvents) return false
+        if (lazyLoadMembersForMessageEvents != other.lazyLoadMembersForMessageEvents) return false
+        if (useThreadNotifications != other.useThreadNotifications) return false
+        if (listOfSupportedEventTypes != other.listOfSupportedEventTypes) return false
+        if (listOfSupportedStateEventTypes != other.listOfSupportedStateEventTypes) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = lazyLoadMembersForStateEvents?.hashCode() ?: 0
+        result = 31 * result + (lazyLoadMembersForMessageEvents?.hashCode() ?: 0)
+        result = 31 * result + (useThreadNotifications?.hashCode() ?: 0)
+        result = 31 * result + (listOfSupportedEventTypes?.hashCode() ?: 0)
+        result = 31 * result + (listOfSupportedStateEventTypes?.hashCode() ?: 0)
+        return result
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 1529064b96..2fb87ca874 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -61,6 +61,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
 import org.matrix.android.sdk.internal.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
@@ -69,7 +70,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
 ) : MatrixRealmMigration(
         dbName = "Session",
-        schemaVersion = 44L,
+        schemaVersion = 45L,
 ) {
     /**
      * Forces all RealmSessionStoreMigration instances to be equal.
@@ -123,5 +124,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 42) MigrateSessionTo042(realm).perform()
         if (oldVersion < 43) MigrateSessionTo043(realm).perform()
         if (oldVersion < 44) MigrateSessionTo044(realm).perform()
+        if (oldVersion < 45) MigrateSessionTo045(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt
new file mode 100644
index 0000000000..645cb41af5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.mapper
+
+import io.realm.RealmList
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+import javax.inject.Inject
+
+internal class FilterParamsMapper @Inject constructor() {
+
+    fun map(entity: SyncFilterParamsEntity): SyncFilterParams {
+        val eventTypes = if (entity.listOfSupportedEventTypesHasBeenSet) {
+            entity.listOfSupportedEventTypes?.toList()
+        } else {
+            null
+        }
+        val stateEventTypes = if (entity.listOfSupportedStateEventTypesHasBeenSet) {
+            entity.listOfSupportedStateEventTypes?.toList()
+        } else {
+            null
+        }
+        return SyncFilterParams(
+                useThreadNotifications = entity.useThreadNotifications,
+                lazyLoadMembersForMessageEvents = entity.lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = entity.lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = eventTypes,
+                listOfSupportedStateEventTypes = stateEventTypes,
+        )
+    }
+
+    fun map(params: SyncFilterParams): SyncFilterParamsEntity {
+        return SyncFilterParamsEntity(
+                useThreadNotifications = params.useThreadNotifications,
+                lazyLoadMembersForMessageEvents = params.lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = params.lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = params.listOfSupportedEventTypes.toRealmList(),
+                listOfSupportedEventTypesHasBeenSet = params.listOfSupportedEventTypes != null,
+                listOfSupportedStateEventTypes = params.listOfSupportedStateEventTypes.toRealmList(),
+                listOfSupportedStateEventTypesHasBeenSet = params.listOfSupportedStateEventTypes != null,
+        )
+    }
+
+    private fun List?.toRealmList(): RealmList? {
+        return this?.toTypedArray()?.let { RealmList(*it) }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt
index c7fda61671..49e9bac18c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt
@@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.migration
 import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
 import org.matrix.android.sdk.internal.database.model.EventEntityFields
-import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
 internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt
new file mode 100644
index 0000000000..d2b43ded28
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo045(realm: DynamicRealm) : RealmMigrator(realm, 45) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("SyncFilterParamsEntity")
+                .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, true)
+                .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, true)
+                .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
+                .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
+                .addField(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, true)
+                .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES.`$`, String::class.java)
+                .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES.`$`, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index b222bcb710..93ff67a911 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -70,7 +70,8 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
             SpaceChildSummaryEntity::class,
             SpaceParentSummaryEntity::class,
             UserPresenceEntity::class,
-            ThreadSummaryEntity::class
+            ThreadSummaryEntity::class,
+            SyncFilterParamsEntity::class,
         ]
 )
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt
new file mode 100644
index 0000000000..e4b62f28e8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.model
+
+import io.realm.RealmList
+import io.realm.RealmObject
+
+/**
+ * This entity stores Sync Filter configuration data, provided by the client.
+ */
+internal open class SyncFilterParamsEntity(
+        var lazyLoadMembersForStateEvents: Boolean? = null,
+        var lazyLoadMembersForMessageEvents: Boolean? = null,
+        var useThreadNotifications: Boolean? = null,
+        var listOfSupportedEventTypes: RealmList? = null,
+        var listOfSupportedEventTypesHasBeenSet: Boolean = false,
+        var listOfSupportedStateEventTypes: RealmList? = null,
+        var listOfSupportedStateEventTypesHasBeenSet: Boolean = false,
+) : RealmObject() {
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
index 1d1bb0e715..4e5b005584 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
@@ -17,74 +17,71 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import com.zhuinden.monarchy.Monarchy
-import io.realm.Realm
 import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.mapper.FilterParamsMapper
 import org.matrix.android.sdk.internal.database.model.FilterEntity
-import org.matrix.android.sdk.internal.database.model.FilterEntityFields
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
 import org.matrix.android.sdk.internal.database.query.get
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import javax.inject.Inject
 
-internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository {
+internal class DefaultFilterRepository @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+        private val filterParamsMapper: FilterParamsMapper
+) : FilterRepository {
 
-    override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean {
-        return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
-            val filterEntity = FilterEntity.get(realm)
-            // Filter has changed, or no filter Id yet
-            filterEntity == null ||
-                    filterEntity.filterBodyJson != filter.toJSONString() ||
-                    filterEntity.filterId.isBlank()
-        }.also { hasChanged ->
-            if (hasChanged) {
-                // Filter is new or has changed, store it and reset the filter Id.
-                // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
-                monarchy.awaitTransaction { realm ->
-                    // We manage only one filter for now
-                    val filterJson = filter.toJSONString()
-                    val roomEventFilterJson = roomEventFilter.toJSONString()
+    override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) {
+        monarchy.awaitTransaction { realm ->
+            // We manage only one filter for now
+            val filterJson = filter.toJSONString()
+            val roomEventFilterJson = roomEventFilter.toJSONString()
 
-                    val filterEntity = FilterEntity.getOrCreate(realm)
+            val filterEntity = FilterEntity.getOrCreate(realm)
 
-                    filterEntity.filterBodyJson = filterJson
-                    filterEntity.roomEventFilterJson = roomEventFilterJson
-                    // Reset filterId
-                    filterEntity.filterId = ""
-                }
-            }
+            filterEntity.filterBodyJson = filterJson
+            filterEntity.roomEventFilterJson = roomEventFilterJson
+            filterEntity.filterId = filterId
         }
     }
 
-    override suspend fun storeFilterId(filter: Filter, filterId: String) {
-        monarchy.awaitTransaction {
-            // We manage only one filter for now
-            val filterJson = filter.toJSONString()
-
-            // Update the filter id, only if the filter body matches
-            it.where()
-                    .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson)
-                    ?.findFirst()
-                    ?.filterId = filterId
+    override suspend fun getStoredSyncFilterBody(): String {
+        return monarchy.awaitTransaction {
+            FilterEntity.getOrCreate(it).filterBodyJson
         }
     }
 
-    override suspend fun getFilter(): String {
+    override suspend fun getStoredSyncFilterId(): String? {
         return monarchy.awaitTransaction {
-            val filter = FilterEntity.getOrCreate(it)
-            if (filter.filterId.isBlank()) {
-                // Use the Json format
-                filter.filterBodyJson
+            val id = FilterEntity.get(it)?.filterId
+            if (id.isNullOrBlank()) {
+                null
             } else {
-                // Use FilterId
-                filter.filterId
+                id
             }
         }
     }
 
-    override suspend fun getRoomFilter(): String {
+    override suspend fun getRoomFilterBody(): String {
         return monarchy.awaitTransaction {
             FilterEntity.getOrCreate(it).roomEventFilterJson
         }
     }
+
+    override suspend fun getStoredFilterParams(): SyncFilterParams? {
+        return monarchy.awaitTransaction { realm ->
+            realm.where().findFirst()?.let {
+                filterParamsMapper.map(it)
+            }
+        }
+    }
+
+    override suspend fun storeFilterParams(params: SyncFilterParams) {
+        return monarchy.awaitTransaction { realm ->
+            val entity = filterParamsMapper.map(params)
+            realm.insertOrUpdate(entity)
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
index 2e68d02d8c..c54e7de07a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
@@ -17,19 +17,27 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import org.matrix.android.sdk.api.session.sync.FilterService
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.task.configureWith
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
 import javax.inject.Inject
 
 internal class DefaultFilterService @Inject constructor(
         private val saveFilterTask: SaveFilterTask,
-        private val taskExecutor: TaskExecutor
+        private val filterRepository: FilterRepository,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
 ) : FilterService {
 
     // TODO Pass a list of support events instead
-    override fun setFilter(filterPreset: FilterService.FilterPreset) {
-        saveFilterTask
-                .configureWith(SaveFilterTask.Params(filterPreset))
-                .executeBy(taskExecutor)
+    override suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) {
+        filterRepository.storeFilterParams(filterBuilder.extractParams())
+
+        // don't upload/store filter until homeserver capabilities are fetched
+        homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.let { homeServerCapabilities ->
+            saveFilterTask.execute(
+                    SaveFilterTask.Params(
+                            filter = filterBuilder.build(homeServerCapabilities)
+                    )
+            )
+        }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index e0919c52e3..1bd2e59e59 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -45,46 +45,7 @@ internal object FilterFactory {
         return FilterUtil.enableLazyLoading(Filter(), true)
     }
 
-    fun createElementFilter(): Filter {
-        return Filter(
-                room = RoomFilter(
-                        timeline = createElementTimelineFilter(),
-                        state = createElementStateFilter()
-                )
-        )
-    }
-
     fun createDefaultRoomFilter(): RoomEventFilter {
         return RoomEventFilter(lazyLoadMembers = true)
     }
-
-    fun createElementRoomFilter(): RoomEventFilter {
-        return RoomEventFilter(
-                lazyLoadMembers = true,
-                // TODO Enable this for optimization
-                // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList()
-        )
-    }
-
-    private fun createElementTimelineFilter(): RoomEventFilter? {
-//        we need to check if homeserver supports thread notifications before setting this param
-//        return RoomEventFilter(enableUnreadThreadNotifications = true)
-        return null
-    }
-
-    private fun createElementStateFilter(): RoomEventFilter {
-        return RoomEventFilter(lazyLoadMembers = true)
-    }
-
-    // Get only managed types by Element
-    private val listOfSupportedEventTypes = listOf(
-            // TODO Complete the list
-            EventType.MESSAGE
-    )
-
-    // Get only managed types by Element
-    private val listOfSupportedStateEventTypes = listOf(
-            // TODO Complete the list
-            EventType.STATE_ROOM_MEMBER
-    )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
index 8531bed1ff..ca9f798fd9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
@@ -44,4 +44,7 @@ internal abstract class FilterModule {
 
     @Binds
     abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask
+
+    @Binds
+    abstract fun bindGetCurrentFilterTask(task: DefaultGetCurrentFilterTask): GetCurrentFilterTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
index f40231c8cf..71d7391e87 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
@@ -16,25 +16,42 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+/**
+ * Repository for request filters.
+ */
 internal interface FilterRepository {
 
     /**
-     * Return true if the filterBody has changed, or need to be sent to the server.
+     * Stores sync filter and room filter.
+     * Note: It looks like we could use [Filter.room.timeline] instead of a separate [RoomEventFilter], but it's not clear if it's safe, so research is needed
+     * @return true if the filterBody has changed, or need to be sent to the server.
      */
-    suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean
+    suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter)
 
     /**
-     * Set the filterId of this filter.
+     * Returns stored sync filter's JSON body if it exists.
      */
-    suspend fun storeFilterId(filter: Filter, filterId: String)
+    suspend fun getStoredSyncFilterBody(): String?
 
     /**
-     * Return filter json or filter id.
+     * Returns stored sync filter's ID if it exists.
      */
-    suspend fun getFilter(): String
+    suspend fun getStoredSyncFilterId(): String?
 
     /**
      * Return the room filter.
      */
-    suspend fun getRoomFilter(): String
+    suspend fun getRoomFilterBody(): String
+
+    /**
+     * Returns filter params stored in local storage if it exists.
+     */
+    suspend fun getStoredFilterParams(): SyncFilterParams?
+
+    /**
+     * Stores filter params to local storage.
+     */
+    suspend fun storeFilterParams(params: SyncFilterParams)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
new file mode 100644
index 0000000000..e88f286e27
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.filter
+
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface GetCurrentFilterTask : Task
+
+internal class DefaultGetCurrentFilterTask @Inject constructor(
+        private val filterRepository: FilterRepository,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
+        private val saveFilterTask: SaveFilterTask
+) : GetCurrentFilterTask {
+
+    override suspend fun execute(params: Unit): String {
+        val storedFilterId = filterRepository.getStoredSyncFilterId()
+        val storedFilterBody = filterRepository.getStoredSyncFilterBody()
+        val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities()
+        val currentFilter = SyncFilterBuilder()
+                .with(filterRepository.getStoredFilterParams())
+                .build(homeServerCapabilities)
+
+        val currentFilterBody = currentFilter.toJSONString()
+
+        return when (storedFilterBody) {
+            currentFilterBody -> storedFilterId ?: storedFilterBody
+            else -> saveFilter(currentFilter)
+        }
+    }
+
+    private suspend fun saveFilter(filter: Filter) = saveFilterTask
+            .execute(
+                    SaveFilterTask.Params(
+                            filter = filter
+                    )
+            )
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
index 63afa1bbbc..82d5ff4d2f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
@@ -16,7 +16,6 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
-import org.matrix.android.sdk.api.session.sync.FilterService
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
@@ -26,10 +25,10 @@ import javax.inject.Inject
 /**
  * Save a filter, in db and if any changes, upload to the server.
  */
-internal interface SaveFilterTask : Task {
+internal interface SaveFilterTask : Task {
 
     data class Params(
-            val filterPreset: FilterService.FilterPreset
+            val filter: Filter
     )
 }
 
@@ -37,33 +36,21 @@ internal class DefaultSaveFilterTask @Inject constructor(
         @UserId private val userId: String,
         private val filterAPI: FilterApi,
         private val filterRepository: FilterRepository,
-        private val globalErrorReceiver: GlobalErrorReceiver
+        private val globalErrorReceiver: GlobalErrorReceiver,
 ) : SaveFilterTask {
 
-    override suspend fun execute(params: SaveFilterTask.Params) {
-        val filterBody = when (params.filterPreset) {
-            FilterService.FilterPreset.ElementFilter -> {
-                FilterFactory.createElementFilter()
-            }
-            FilterService.FilterPreset.NoFilter -> {
-                FilterFactory.createDefaultFilter()
-            }
-        }
-        val roomFilter = when (params.filterPreset) {
-            FilterService.FilterPreset.ElementFilter -> {
-                FilterFactory.createElementRoomFilter()
-            }
-            FilterService.FilterPreset.NoFilter -> {
-                FilterFactory.createDefaultRoomFilter()
-            }
-        }
-        val updated = filterRepository.storeFilter(filterBody, roomFilter)
-        if (updated) {
-            val filterResponse = executeRequest(globalErrorReceiver) {
-                // TODO auto retry
-                filterAPI.uploadFilter(userId, filterBody)
-            }
-            filterRepository.storeFilterId(filterBody, filterResponse.filterId)
+    override suspend fun execute(params: SaveFilterTask.Params): String {
+        val filter = params.filter
+        val filterResponse = executeRequest(globalErrorReceiver) {
+            // TODO auto retry
+            filterAPI.uploadFilter(userId, filter)
         }
+
+        filterRepository.storeSyncFilter(
+                filter = filter,
+                filterId = filterResponse.filterId,
+                roomEventFilter = FilterFactory.createDefaultRoomFilter()
+        )
+        return filterResponse.filterId
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt
index 96646b42ed..9d8d8ecbf1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt
@@ -47,7 +47,7 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor(
 ) : FetchTokenAndPaginateTask {
 
     override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result {
-        val filter = filterRepository.getRoomFilter()
+        val filter = filterRepository.getRoomFilterBody()
         val response = executeRequest(globalErrorReceiver) {
             roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
index 015e55f070..c3911dfa2c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
@@ -39,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor(
 ) : GetContextOfEventTask {
 
     override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
-        val filter = filterRepository.getRoomFilter()
+        val filter = filterRepository.getRoomFilterBody()
         val response = executeRequest(globalErrorReceiver) {
             // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process.
             roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
index 8aeccb66c8..1a7b1cdac4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
@@ -41,7 +41,7 @@ internal class DefaultPaginationTask @Inject constructor(
 ) : PaginationTask {
 
     override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result {
-        val filter = filterRepository.getRoomFilter()
+        val filter = filterRepository.getRoomFilterBody()
         val chunk = executeRequest(
                 globalErrorReceiver,
                 canRetry = true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
index bc1a69769d..8a287fb0b4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
@@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.network.toFailure
 import org.matrix.android.sdk.internal.session.SessionListeners
 import org.matrix.android.sdk.internal.session.dispatchTo
-import org.matrix.android.sdk.internal.session.filter.FilterRepository
+import org.matrix.android.sdk.internal.session.filter.GetCurrentFilterTask
 import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask
 import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser
 import org.matrix.android.sdk.internal.session.user.UserStore
@@ -64,11 +64,9 @@ internal interface SyncTask : Task {
 internal class DefaultSyncTask @Inject constructor(
         private val syncAPI: SyncAPI,
         @UserId private val userId: String,
-        private val filterRepository: FilterRepository,
         private val syncResponseHandler: SyncResponseHandler,
         private val syncRequestStateTracker: SyncRequestStateTracker,
         private val syncTokenStore: SyncTokenStore,
-        private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
         private val userStore: UserStore,
         private val session: Session,
         private val sessionListeners: SessionListeners,
@@ -79,6 +77,8 @@ internal class DefaultSyncTask @Inject constructor(
         private val syncResponseParser: InitialSyncResponseParser,
         private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
         private val clock: Clock,
+        private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
+        private val getCurrentFilterTask: GetCurrentFilterTask
 ) : SyncTask {
 
     private val workingDir = File(fileDirectory, "is")
@@ -100,8 +100,13 @@ internal class DefaultSyncTask @Inject constructor(
             requestParams["since"] = token
             timeout = params.timeout
         }
+
+        // Maybe refresh the homeserver capabilities data we know
+        getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
+        val filter = getCurrentFilterTask.execute(Unit)
+
         requestParams["timeout"] = timeout.toString()
-        requestParams["filter"] = filterRepository.getFilter()
+        requestParams["filter"] = filter
         params.presence?.let { requestParams["set_presence"] = it.value }
 
         val isInitialSync = token == null
@@ -115,8 +120,6 @@ internal class DefaultSyncTask @Inject constructor(
             )
             syncRequestStateTracker.startRoot(InitialSyncStep.ImportingAccount, 100)
         }
-        // Maybe refresh the homeserver capabilities data we know
-        getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
 
         val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt
new file mode 100644
index 0000000000..a7de7f5579
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.sync.filter
+
+internal data class SyncFilterParams(
+        val lazyLoadMembersForStateEvents: Boolean? = null,
+        val lazyLoadMembersForMessageEvents: Boolean? = null,
+        val useThreadNotifications: Boolean? = null,
+        val listOfSupportedEventTypes: List? = null,
+        val listOfSupportedStateEventTypes: List? = null,
+)
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt
new file mode 100644
index 0000000000..201423685c
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.sync
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.filter.DefaultGetCurrentFilterTask
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+import org.matrix.android.sdk.test.fakes.FakeFilterRepository
+import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource
+import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask
+
+private const val A_FILTER_ID = "filter-id"
+private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities()
+private val A_SYNC_FILTER_PARAMS = SyncFilterParams(
+        lazyLoadMembersForMessageEvents = true,
+        lazyLoadMembersForStateEvents = true,
+        useThreadNotifications = true
+)
+
+@ExperimentalCoroutinesApi
+class DefaultGetCurrentFilterTaskTest {
+
+    private val filterRepository = FakeFilterRepository()
+    private val homeServerCapabilitiesDataSource = FakeHomeServerCapabilitiesDataSource()
+    private val saveFilterTask = FakeSaveFilterTask()
+
+    private val getCurrentFilterTask = DefaultGetCurrentFilterTask(
+            filterRepository = filterRepository,
+            homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance,
+            saveFilterTask = saveFilterTask
+    )
+
+    @Test
+    fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest {
+        filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
+
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
+
+        filterRepository.givenFilterStored(null, null)
+
+        getCurrentFilterTask.execute(Unit)
+
+        val filter = SyncFilterBuilder()
+                .with(A_SYNC_FILTER_PARAMS)
+                .build(A_HOMESERVER_CAPABILITIES)
+
+        saveFilterTask.verifyExecution(filter)
+    }
+
+    @Test
+    fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest {
+        filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
+
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
+
+        val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
+        filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString())
+
+        val result = getCurrentFilterTask.execute(Unit)
+
+        result shouldBeEqualTo A_FILTER_ID
+    }
+
+    @Test
+    fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest {
+        filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
+
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
+
+        val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
+        filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString())
+
+        val newHomeServerCapabilities = HomeServerCapabilities(canUseThreadReadReceiptsAndNotifications = true)
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(newHomeServerCapabilities)
+        val newFilter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(newHomeServerCapabilities)
+
+        getCurrentFilterTask.execute(Unit)
+
+        saveFilterTask.verifyExecution(newFilter)
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt
new file mode 100644
index 0000000000..b8225f21d6
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.filter.FilterRepository
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+internal class FakeFilterRepository : FilterRepository by mockk() {
+
+    fun givenFilterStored(filterId: String?, filterBody: String?) {
+        coEvery { getStoredSyncFilterId() } returns filterId
+        coEvery { getStoredSyncFilterBody() } returns filterBody
+    }
+
+    fun givenFilterParamsAreStored(syncFilterParams: SyncFilterParams?) {
+        coEvery { getStoredFilterParams() } returns syncFilterParams
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt
new file mode 100644
index 0000000000..9a56a599d1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
+
+internal class FakeHomeServerCapabilitiesDataSource {
+    val instance = mockk()
+
+    fun givenHomeServerCapabilities(homeServerCapabilities: HomeServerCapabilities) {
+        every { instance.getHomeServerCapabilities() } returns homeServerCapabilities
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt
new file mode 100644
index 0000000000..40bee227e0
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import io.mockk.slot
+import org.amshove.kluent.shouldBeEqualTo
+import org.matrix.android.sdk.internal.session.filter.Filter
+import org.matrix.android.sdk.internal.session.filter.SaveFilterTask
+import java.util.UUID
+
+internal class FakeSaveFilterTask : SaveFilterTask by mockk() {
+
+    init {
+        coEvery { execute(any()) } returns UUID.randomUUID().toString()
+    }
+
+    fun verifyExecution(filter: Filter) {
+        val slot = slot()
+        coVerify { execute(capture(slot)) }
+        val params = slot.captured
+        params.filter shouldBeEqualTo filter
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
index c47769052c..96c3f8a6ce 100644
--- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
@@ -24,9 +24,9 @@ import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.features.call.webrtc.WebRtcCallManager
 import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.sync.SyncUtils
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.sync.FilterService
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -41,7 +41,9 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
     fun execute(session: Session, startSyncing: Boolean = true) {
         Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing")
         session.open()
-        session.filterService().setFilter(FilterService.FilterPreset.ElementFilter)
+        session.coroutineScope.launch {
+            session.filterService().setSyncFilter(SyncUtils.getSyncFilterBuilder())
+        }
         if (startSyncing) {
             session.startSyncing(context)
         }
diff --git a/vector/src/main/java/im/vector/app/features/sync/SyncUtils.kt b/vector/src/main/java/im/vector/app/features/sync/SyncUtils.kt
new file mode 100644
index 0000000000..e3408d8814
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/sync/SyncUtils.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.sync
+
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+
+object SyncUtils {
+    // Get only managed types by Element
+    private val listOfSupportedTimelineEventTypes = listOf(
+            // TODO Complete the list
+            EventType.MESSAGE
+    )
+
+    // Get only managed types by Element
+    private val listOfSupportedStateEventTypes = listOf(
+            // TODO Complete the list
+            EventType.STATE_ROOM_MEMBER
+    )
+
+    fun getSyncFilterBuilder(): SyncFilterBuilder {
+        return SyncFilterBuilder()
+                .useThreadNotifications(true)
+                .lazyLoadMembersForStateEvents(true)
+        /**
+         * Currently we don't set [lazy_load_members = true] for Filter.room.timeline even though we set it for RoomFilter which is used later to
+         * fetch messages in a room. It's not clear if it's done so by mistake or intentionally, so changing it could case side effects and need
+         * careful testing
+         * */
+//                .lazyLoadMembersForMessageEvents(true)
+//                .listOfSupportedStateEventTypes(listOfSupportedStateEventTypes)
+//                .listOfSupportedTimelineEventTypes(listOfSupportedTimelineEventTypes)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
index 760879b69d..01596e796d 100644
--- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
@@ -19,6 +19,7 @@ package im.vector.app.core.session
 import im.vector.app.core.extensions.startSyncing
 import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.features.session.coroutineScope
+import im.vector.app.features.sync.SyncUtils
 import im.vector.app.test.fakes.FakeContext
 import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater
 import im.vector.app.test.fakes.FakeSession
@@ -38,7 +39,6 @@ import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
-import org.matrix.android.sdk.api.session.sync.FilterService
 
 class ConfigureAndStartSessionUseCaseTest {
 
@@ -83,7 +83,7 @@ class ConfigureAndStartSessionUseCaseTest {
 
         // Then
         verify { fakeSession.startSyncing(fakeContext.instance) }
-        fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
+        fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
         fakeSession.fakePushersService.verifyRefreshPushers()
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
         coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
@@ -105,7 +105,7 @@ class ConfigureAndStartSessionUseCaseTest {
 
         // Then
         verify { fakeSession.startSyncing(fakeContext.instance) }
-        fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
+        fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
         fakeSession.fakePushersService.verifyRefreshPushers()
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
         coVerify(inverse = true) { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
@@ -127,7 +127,7 @@ class ConfigureAndStartSessionUseCaseTest {
 
         // Then
         verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) }
-        fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
+        fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
         fakeSession.fakePushersService.verifyRefreshPushers()
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
         coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt
index 4332368127..9be59d31fd 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt
@@ -16,20 +16,21 @@
 
 package im.vector.app.test.fakes
 
-import io.mockk.every
+import io.mockk.coEvery
+import io.mockk.coVerify
 import io.mockk.just
 import io.mockk.mockk
 import io.mockk.runs
-import io.mockk.verify
 import org.matrix.android.sdk.api.session.sync.FilterService
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
 
 class FakeFilterService : FilterService by mockk() {
 
     fun givenSetFilterSucceeds() {
-        every { setFilter(any()) } just runs
+        coEvery { setSyncFilter(any()) } just runs
     }
 
-    fun verifySetFilter(filterPreset: FilterService.FilterPreset) {
-        verify { setFilter(filterPreset) }
+    fun verifySetSyncFilter(filterBuilder: SyncFilterBuilder) {
+        coVerify { setSyncFilter(filterBuilder) }
     }
 }

From dd815840764e21cd874985831ae6b6627ff00e0e Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Mon, 28 Nov 2022 12:12:49 +0100
Subject: [PATCH 360/679] Ad default value to MessageStickerContent.body in
 case of redaction

---
 .../sdk/api/session/room/model/message/MessageStickerContent.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
index f8c1c0d798..627ce53df6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
@@ -34,7 +34,7 @@ data class MessageStickerContent(
          * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
          * or some kind of content description for accessibility e.g. 'image attachment'.
          */
-        @Json(name = "body") override val body: String,
+        @Json(name = "body") override val body: String = "",
 
         /**
          * Metadata about the image referred to in url.

From ee22dafbc90732003f5944c95a577bcf75ddb7fb Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Mon, 28 Nov 2022 12:14:31 +0100
Subject: [PATCH 361/679] Fix regression when getting last message content for
 Voice Broadcast state event

---
 .../main/java/im/vector/app/core/extensions/TimelineEvent.kt   | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
index 63144ca1b3..c94f9cd921 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
@@ -19,7 +19,6 @@ package im.vector.app.core.extensions
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.send.SendState
@@ -41,7 +40,7 @@ fun TimelineEvent.getVectorLastMessageContent(): MessageContent? {
     // Iterate on event types which are not part of the matrix sdk, otherwise fallback to the sdk method
     return when (root.getClearType()) {
         VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
-            (annotations?.editSummary?.latestEdit?.getClearContent()?.toModel().toContent().toModel()
+            (annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()
                     ?: root.getClearContent().toModel())
         }
         else -> getLastMessageContent()

From d04e2b0f8264346b64360017fc6bf77e91a2d3dd Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Mon, 28 Nov 2022 12:40:17 +0100
Subject: [PATCH 362/679] Trigger CI


From 2d60e49205b28a6d8199060afdf9089eceed72e9 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 23 Nov 2022 12:09:41 +0100
Subject: [PATCH 363/679] Handle redaction when observing voice broadcast state
 changes

---
 .../listening/VoiceBroadcastPlayerImpl.kt     |   4 +-
 .../GetLiveVoiceBroadcastChunksUseCase.kt     |   4 +-
 ...stRecentVoiceBroadcastStateEventUseCase.kt | 163 ++++++++++++++++++
 .../usecase/GetVoiceBroadcastEventUseCase.kt  |  68 --------
 4 files changed, 167 insertions(+), 72 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
 delete mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index 5b0e5b2b1c..79d59064e9 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -30,7 +30,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat
 import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
 import im.vector.lib.core.utils.timer.CountUpTimer
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.launchIn
@@ -48,7 +48,7 @@ import javax.inject.Singleton
 class VoiceBroadcastPlayerImpl @Inject constructor(
         private val sessionHolder: ActiveSessionHolder,
         private val playbackTracker: AudioMessagePlaybackTracker,
-        private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
+        private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
         private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
 ) : VoiceBroadcastPlayer {
 
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
index 16b15b9a77..03e713eeaa 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
@@ -24,7 +24,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.sequence
-import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
 import im.vector.app.features.voicebroadcast.voiceBroadcastId
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -48,7 +48,7 @@ import javax.inject.Inject
  */
 class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
-        private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
+        private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
 ) {
 
     fun execute(voiceBroadcast: VoiceBroadcast): Flow> {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
new file mode 100644
index 0000000000..b882d1625b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.voicebroadcast.usecase
+
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.voiceBroadcastId
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.transformWhile
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.flow.flow
+import org.matrix.android.sdk.flow.mapOptional
+import timber.log.Timber
+import javax.inject.Inject
+
+class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor(
+        private val session: Session,
+) {
+
+    fun execute(voiceBroadcast: VoiceBroadcast): Flow> {
+        val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
+        return getMostRecentVoiceBroadcastEventFlow(room, voiceBroadcast)
+                .onEach { event ->
+                    Timber.d(
+                            "## VoiceBroadcast | " +
+                                    "voiceBroadcastId=${event.getOrNull()?.voiceBroadcastId}, " +
+                                    "state=${event.getOrNull()?.content?.voiceBroadcastState}"
+                    )
+                }
+    }
+
+    /**
+     * Get a flow of the most recent event for the given voice broadcast.
+     */
+    private fun getMostRecentVoiceBroadcastEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow> {
+        val startedEventFlow = room.flow().liveTimelineEvent(voiceBroadcast.voiceBroadcastId)
+        // observe started event changes
+        return startedEventFlow
+                .mapOptional { it.root.asVoiceBroadcastEvent() }
+                .flatMapLatest { startedEvent ->
+                    if (startedEvent.hasValue().not() || startedEvent.get().root.isRedacted()) {
+                        // if started event is null or redacted, send null
+                        flowOf(Optional.empty())
+                    } else {
+                        // otherwise, observe most recent event changes
+                        getMostRecentRelatedEventFlow(room, voiceBroadcast)
+                                .transformWhile { mostRecentEvent ->
+                                    emit(mostRecentEvent)
+                                    mostRecentEvent.hasValue()
+                                }
+                                .map {
+                                    if (!it.hasValue()) {
+                                        // no most recent event, fallback to started event
+                                        startedEvent
+                                    } else {
+                                        // otherwise, keep the most recent event
+                                        it
+                                    }
+                                }
+                    }
+                }
+                .distinctUntilChangedBy { it.getOrNull()?.content?.voiceBroadcastState }
+    }
+
+    /**
+     * Get a flow of the most recent related event.
+     */
+    private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow> {
+        val mostRecentEvent = getMostRecentRelatedEvent(room, voiceBroadcast).toOptional()
+        return if (mostRecentEvent.hasValue()) {
+            val stateKey = mostRecentEvent.get().root.stateKey.orEmpty()
+            // observe incoming voice broadcast state events
+            room.flow()
+                    .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(stateKey))
+                    .mapOptional { it.asVoiceBroadcastEvent() }
+                    // drop first event sent by the matrix-sdk, we compute manually this first event
+                    .drop(1)
+                    // start with the computed most recent event
+                    .onStart { emit(mostRecentEvent) }
+                    // handle event if null or related to the given voice broadcast
+                    .filter { it.hasValue().not() || it.get().voiceBroadcastId == voiceBroadcast.voiceBroadcastId }
+                    // observe changes while event is not null
+                    .transformWhile { event ->
+                        emit(event)
+                        event.hasValue()
+                    }
+                    .flatMapLatest { newMostRecentEvent ->
+                        if (newMostRecentEvent.hasValue()) {
+                            // observe most recent event changes
+                            newMostRecentEvent.get().flow()
+                                    .transformWhile { event ->
+                                        // observe changes until event is null or redacted
+                                        emit(event)
+                                        event.hasValue() && event.get().root.isRedacted().not()
+                                    }
+                                    .flatMapLatest { event ->
+                                        if (event.getOrNull()?.root?.isRedacted().orFalse()) {
+                                            // event is null or redacted, switch to the latest not redacted event
+                                            getMostRecentRelatedEventFlow(room, voiceBroadcast)
+                                        } else {
+                                            // event is not redacted, send the event
+                                            flowOf(event)
+                                        }
+                                    }
+                        } else {
+                            // there is no more most recent event, just send it
+                            flowOf(newMostRecentEvent)
+                        }
+                    }
+        } else {
+            // there is no more most recent event, just send it
+            flowOf(mostRecentEvent)
+        }
+    }
+
+    /**
+     * Get the most recent event related to the given voice broadcast.
+     */
+    private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
+        return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
+                .mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } }
+                .maxByOrNull { it.root.originServerTs ?: 0 }
+    }
+
+    /**
+     * Get a flow of the given voice broadcast event changes.
+     */
+    private fun VoiceBroadcastEvent.flow(): Flow> {
+        val room = this.root.roomId?.let { session.getRoom(it) } ?: return flowOf(Optional.empty())
+        return room.flow().liveTimelineEvent(root.eventId!!).mapOptional { it.root.asVoiceBroadcastEvent() }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt
deleted file mode 100644
index 94eca2b54e..0000000000
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (c) 2022 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.app.features.voicebroadcast.usecase
-
-import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.voiceBroadcastId
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.onStart
-import org.matrix.android.sdk.api.query.QueryStringValue
-import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.events.model.RelationType
-import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.api.util.toOptional
-import org.matrix.android.sdk.flow.flow
-import org.matrix.android.sdk.flow.mapOptional
-import timber.log.Timber
-import javax.inject.Inject
-
-class GetVoiceBroadcastEventUseCase @Inject constructor(
-        private val session: Session,
-) {
-
-    fun execute(voiceBroadcast: VoiceBroadcast): Flow> {
-        val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
-
-        Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast")
-
-        val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent()
-        val latestEvent = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
-                .mapNotNull { it.root.asVoiceBroadcastEvent() }
-                .maxByOrNull { it.root.originServerTs ?: 0 }
-                ?: initialEvent
-
-        return when (latestEvent?.content?.voiceBroadcastState) {
-            null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional())
-            else -> {
-                room.flow()
-                        .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty()))
-                        .onStart { emit(latestEvent.root.toOptional()) }
-                        .distinctUntilChanged()
-                        .filter { !it.hasValue() || it.getOrNull()?.asVoiceBroadcastEvent()?.voiceBroadcastId == voiceBroadcast.voiceBroadcastId }
-                        .mapOptional { it.asVoiceBroadcastEvent() }
-            }
-        }
-    }
-}

From f436de12300b766fadfaf2c93300ac8bf9e01614 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 23 Nov 2022 14:10:44 +0100
Subject: [PATCH 364/679] Handle voice broadcast deletion on listener side

---
 .../listening/VoiceBroadcastPlayerImpl.kt      | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index 79d59064e9..f04b85859b 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -145,19 +145,25 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
 
         playingState = State.BUFFERING
 
-        observeVoiceBroadcastLiveState(voiceBroadcast)
+        observeVoiceBroadcastStateEvent(voiceBroadcast)
         fetchPlaylistAndStartPlayback(voiceBroadcast)
     }
 
-    private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
+    private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) {
         voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
-                .onEach {
-                    currentVoiceBroadcastEvent = it.getOrNull()
-                    updateLiveListeningMode()
-                }
+                .onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) }
                 .launchIn(sessionScope)
     }
 
+    private fun onVoiceBroadcastStateEventUpdated(event: VoiceBroadcastEvent?) {
+        if (event == null) {
+            stop()
+        } else {
+            currentVoiceBroadcastEvent = event
+            updateLiveListeningMode()
+        }
+    }
+
     private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
         fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
                 .onEach {

From 763b60ee6b5dae8b8aa7c7b5e7ccb9654b38bdd5 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 23 Nov 2022 17:31:29 +0100
Subject: [PATCH 365/679] Update voice broadcast recorder according to the most
 recent voice broadcast state event

---
 .../java/im/vector/app/core/di/VoiceModule.kt | 13 ++++-
 .../recording/VoiceBroadcastRecorder.kt       |  3 +-
 .../recording/VoiceBroadcastRecorderQ.kt      | 58 ++++++++++++++++---
 .../usecase/PauseVoiceBroadcastUseCase.kt     |  6 --
 .../usecase/ResumeVoiceBroadcastUseCase.kt    | 10 +---
 .../usecase/StartVoiceBroadcastUseCase.kt     | 19 ++++--
 .../ResumeVoiceBroadcastUseCaseTest.kt        |  5 +-
 7 files changed, 78 insertions(+), 36 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
index 30a8565771..6437326294 100644
--- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
@@ -27,6 +27,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
 import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl
 import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
+import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
 import javax.inject.Singleton
 
 @InstallIn(SingletonComponent::class)
@@ -36,9 +37,17 @@ abstract class VoiceModule {
     companion object {
         @Provides
         @Singleton
-        fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
+        fun providesVoiceBroadcastRecorder(
+                context: Context,
+                sessionHolder: ActiveSessionHolder,
+                getMostRecentVoiceBroadcastStateEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
+        ): VoiceBroadcastRecorder? {
             return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-                VoiceBroadcastRecorderQ(context)
+                VoiceBroadcastRecorderQ(
+                        context = context,
+                        sessionHolder = sessionHolder,
+                        getVoiceBroadcastEventUseCase = getMostRecentVoiceBroadcastStateEventUseCase
+                )
             } else {
                 null
             }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
index bc13d1fea8..00e4bb17dd 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.voicebroadcast.recording
 
 import androidx.annotation.IntRange
 import im.vector.app.features.voice.VoiceRecorder
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import java.io.File
 
 interface VoiceBroadcastRecorder : VoiceRecorder {
@@ -31,7 +32,7 @@ interface VoiceBroadcastRecorder : VoiceRecorder {
     /** Current remaining time of recording, in seconds, if any. */
     val currentRemainingTime: Long?
 
-    fun startRecord(roomId: String, chunkLength: Int, maxLength: Int)
+    fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int)
     fun addListener(listener: Listener)
     fun removeListener(listener: Listener)
 
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
index c5408b768b..483b88f57c 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
@@ -20,8 +20,17 @@ import android.content.Context
 import android.media.MediaRecorder
 import android.os.Build
 import androidx.annotation.RequiresApi
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.voice.AbstractVoiceRecorderQ
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
 import im.vector.lib.core.utils.timer.CountUpTimer
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
 import java.util.concurrent.CopyOnWriteArrayList
@@ -30,10 +39,17 @@ import java.util.concurrent.TimeUnit
 @RequiresApi(Build.VERSION_CODES.Q)
 class VoiceBroadcastRecorderQ(
         context: Context,
+        private val sessionHolder: ActiveSessionHolder,
+        private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase
 ) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder {
 
+    private val session get() = sessionHolder.getActiveSession()
+    private val sessionScope get() = session.coroutineScope
+
+    private var voiceBroadcastStateObserver: Job? = null
+
     private var maxFileSize = 0L // zero or negative for no limit
-    private var currentRoomId: String? = null
+    private var currentVoiceBroadcast: VoiceBroadcast? = null
     private var currentMaxLength: Int = 0
 
     override var currentSequence = 0
@@ -68,14 +84,16 @@ class VoiceBroadcastRecorderQ(
         }
     }
 
-    override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) {
-        currentRoomId = roomId
+    override fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) {
+        // Stop recording previous voice broadcast if any
+        if (recordingState != VoiceBroadcastRecorder.State.Idle) stopRecord()
+
+        currentVoiceBroadcast = voiceBroadcast
         maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
         currentMaxLength = maxLength
         currentSequence = 1
-        startRecord(roomId)
-        recordingState = VoiceBroadcastRecorder.State.Recording
-        recordingTicker.start()
+
+        observeVoiceBroadcastStateEvent(voiceBroadcast)
     }
 
     override fun pauseRecord() {
@@ -88,7 +106,7 @@ class VoiceBroadcastRecorderQ(
 
     override fun resumeRecord() {
         currentSequence++
-        currentRoomId?.let { startRecord(it) }
+        currentVoiceBroadcast?.let { startRecord(it.roomId) }
         recordingState = VoiceBroadcastRecorder.State.Recording
         recordingTicker.resume()
     }
@@ -104,11 +122,15 @@ class VoiceBroadcastRecorderQ(
         // Remove listeners
         listeners.clear()
 
+        // Do not observe anymore voice broadcast changes
+        voiceBroadcastStateObserver?.cancel()
+        voiceBroadcastStateObserver = null
+
         // Reset data
         currentSequence = 0
         currentMaxLength = 0
         currentRemainingTime = null
-        currentRoomId = null
+        currentVoiceBroadcast = null
     }
 
     override fun release() {
@@ -126,6 +148,26 @@ class VoiceBroadcastRecorderQ(
         listeners.remove(listener)
     }
 
+    private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) {
+        voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
+                .onEach { onVoiceBroadcastStateEventUpdated(voiceBroadcast, it.getOrNull()) }
+                .launchIn(sessionScope)
+    }
+
+    private fun onVoiceBroadcastStateEventUpdated(voiceBroadcast: VoiceBroadcast, event: VoiceBroadcastEvent?) {
+        when (event?.content?.voiceBroadcastState) {
+            VoiceBroadcastState.STARTED -> {
+                startRecord(voiceBroadcast.roomId)
+                recordingState = VoiceBroadcastRecorder.State.Recording
+                recordingTicker.start()
+            }
+            VoiceBroadcastState.PAUSED -> pauseRecord()
+            VoiceBroadcastState.RESUMED -> resumeRecord()
+            VoiceBroadcastState.STOPPED,
+            null -> stopRecord()
+        }
+    }
+
     private fun onMaxFileSizeApproaching(roomId: String) {
         setNextOutputFile(roomId)
     }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
index 58e1f26f44..3ce6e4a533 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
@@ -62,11 +62,5 @@ class PauseVoiceBroadcastUseCase @Inject constructor(
                         lastChunkSequence = voiceBroadcastRecorder?.currentSequence,
                 ).toContent(),
         )
-
-        pauseRecording()
-    }
-
-    private fun pauseRecording() {
-        voiceBroadcastRecorder?.pauseRecord()
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
index 524b64e095..5ad5b0704d 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
@@ -20,7 +20,6 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.toContent
@@ -31,8 +30,7 @@ import timber.log.Timber
 import javax.inject.Inject
 
 class ResumeVoiceBroadcastUseCase @Inject constructor(
-        private val session: Session,
-        private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
+        private val session: Session
 ) {
 
     suspend fun execute(roomId: String): Result = runCatching {
@@ -66,11 +64,5 @@ class ResumeVoiceBroadcastUseCase @Inject constructor(
                         voiceBroadcastStateStr = VoiceBroadcastState.RESUMED.value,
                 ).toContent(),
         )
-
-        resumeRecording()
-    }
-
-    private fun resumeRecording() {
-        voiceBroadcastRecorder?.resumeRecord()
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
index 45f622ad92..20f0615863 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
@@ -24,11 +24,13 @@ import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
 import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
 import im.vector.lib.multipicker.utils.toMultiPickerAudioType
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import org.jetbrains.annotations.VisibleForTesting
 import org.matrix.android.sdk.api.query.QueryStringValue
@@ -43,6 +45,8 @@ import org.matrix.android.sdk.api.session.room.getStateEvent
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
 import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
+import org.matrix.android.sdk.flow.flow
+import org.matrix.android.sdk.flow.unwrap
 import timber.log.Timber
 import java.io.File
 import javax.inject.Inject
@@ -63,6 +67,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
 
         assertCanStartVoiceBroadcast(room)
         startVoiceBroadcast(room)
+        return Result.success(Unit)
     }
 
     private suspend fun startVoiceBroadcast(room: Room) {
@@ -79,13 +84,15 @@ class StartVoiceBroadcastUseCase @Inject constructor(
                 ).toContent()
         )
 
-        startRecording(room, eventId, chunkLength, maxLength)
+        val voiceBroadcast = VoiceBroadcast(roomId = room.roomId, voiceBroadcastId = eventId)
+        room.flow().liveTimelineEvent(eventId).unwrap().first() // wait for the event come back from the sync
+        startRecording(room, voiceBroadcast, chunkLength, maxLength)
     }
 
-    private fun startRecording(room: Room, eventId: String, chunkLength: Int, maxLength: Int) {
+    private fun startRecording(room: Room, voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) {
         voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener {
             override fun onVoiceMessageCreated(file: File, sequence: Int) {
-                sendVoiceFile(room, file, eventId, sequence)
+                sendVoiceFile(room, file, voiceBroadcast, sequence)
             }
 
             override fun onRemainingTimeUpdated(remainingTime: Long?) {
@@ -94,10 +101,10 @@ class StartVoiceBroadcastUseCase @Inject constructor(
                 }
             }
         })
-        voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength, maxLength)
+        voiceBroadcastRecorder?.startRecordVoiceBroadcast(voiceBroadcast, chunkLength, maxLength)
     }
 
-    private fun sendVoiceFile(room: Room, voiceMessageFile: File, referenceEventId: String, sequence: Int) {
+    private fun sendVoiceFile(room: Room, voiceMessageFile: File, voiceBroadcast: VoiceBroadcast, sequence: Int) {
         val outputFileUri = FileProvider.getUriForFile(
                 context,
                 buildMeta.applicationId + ".fileProvider",
@@ -109,7 +116,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
                 attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
                 compressBeforeSending = false,
                 roomIds = emptySet(),
-                relatesTo = RelationDefaultContent(RelationType.REFERENCE, referenceEventId),
+                relatesTo = RelationDefaultContent(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId),
                 additionalContent = mapOf(
                         VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY to VoiceBroadcastChunk(sequence = sequence).toContent()
                 )
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt
index 8b66d45dd4..7fe74052a9 100644
--- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt
@@ -19,7 +19,6 @@ package im.vector.app.features.voicebroadcast.usecase
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
 import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
 import im.vector.app.test.fakes.FakeRoom
 import im.vector.app.test.fakes.FakeRoomService
@@ -27,7 +26,6 @@ import im.vector.app.test.fakes.FakeSession
 import io.mockk.clearAllMocks
 import io.mockk.coEvery
 import io.mockk.coVerify
-import io.mockk.mockk
 import io.mockk.slot
 import kotlinx.coroutines.test.runTest
 import org.amshove.kluent.shouldBe
@@ -47,8 +45,7 @@ class ResumeVoiceBroadcastUseCaseTest {
 
     private val fakeRoom = FakeRoom()
     private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
-    private val fakeVoiceBroadcastRecorder = mockk(relaxed = true)
-    private val resumeVoiceBroadcastUseCase = ResumeVoiceBroadcastUseCase(fakeSession, fakeVoiceBroadcastRecorder)
+    private val resumeVoiceBroadcastUseCase = ResumeVoiceBroadcastUseCase(fakeSession)
 
     @Test
     fun `given a room id with a potential existing voice broadcast state when calling execute then the voice broadcast is resumed or not`() = runTest {

From 3ebcd8c1f4274f86c2d55052e61941be80978b3e Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 23 Nov 2022 17:36:59 +0100
Subject: [PATCH 366/679] changelog

---
 changelog.d/7629.wip | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7629.wip

diff --git a/changelog.d/7629.wip b/changelog.d/7629.wip
new file mode 100644
index 0000000000..ecc4449b6f
--- /dev/null
+++ b/changelog.d/7629.wip
@@ -0,0 +1 @@
+Voice Broadcast - Handle redaction of the state events on the listener and recorder sides

From 023326a20db38885bad98449b3c4e345528b968f Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 24 Nov 2022 10:18:58 +0100
Subject: [PATCH 367/679] Do not wait for state event feedback for pause/stop
 actions on the recorder

---
 .../voicebroadcast/recording/VoiceBroadcastRecorderQ.kt   | 2 ++
 .../recording/usecase/PauseVoiceBroadcastUseCase.kt       | 8 ++++++++
 .../recording/usecase/StopVoiceBroadcastUseCase.kt        | 6 ++++--
 3 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
index 483b88f57c..b751417ca6 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
@@ -97,6 +97,7 @@ class VoiceBroadcastRecorderQ(
     }
 
     override fun pauseRecord() {
+        if (recordingState != VoiceBroadcastRecorder.State.Recording) return
         tryOrNull { mediaRecorder?.stop() }
         mediaRecorder?.reset()
         recordingState = VoiceBroadcastRecorder.State.Paused
@@ -105,6 +106,7 @@ class VoiceBroadcastRecorderQ(
     }
 
     override fun resumeRecord() {
+        if (recordingState != VoiceBroadcastRecorder.State.Paused) return
         currentSequence++
         currentVoiceBroadcast?.let { startRecord(it.roomId) }
         recordingState = VoiceBroadcastRecorder.State.Recording
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
index 3ce6e4a533..817c1a72e4 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
@@ -53,6 +53,10 @@ class PauseVoiceBroadcastUseCase @Inject constructor(
 
     private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
         Timber.d("## PauseVoiceBroadcastUseCase: Send new voice broadcast info state event")
+
+        // immediately pause the recording
+        pauseRecording()
+
         room.stateService().sendStateEvent(
                 eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
                 stateKey = session.myUserId,
@@ -63,4 +67,8 @@ class PauseVoiceBroadcastUseCase @Inject constructor(
                 ).toContent(),
         )
     }
+
+    private fun pauseRecording() {
+        voiceBroadcastRecorder?.pauseRecord()
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
index da13100609..cd70671e76 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
@@ -54,6 +54,10 @@ class StopVoiceBroadcastUseCase @Inject constructor(
 
     private suspend fun stopVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
         Timber.d("## StopVoiceBroadcastUseCase: Send new voice broadcast info state event")
+
+        // Immediately stop the recording
+        stopRecording()
+
         room.stateService().sendStateEvent(
                 eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
                 stateKey = session.myUserId,
@@ -63,8 +67,6 @@ class StopVoiceBroadcastUseCase @Inject constructor(
                         lastChunkSequence = voiceBroadcastRecorder?.currentSequence,
                 ).toContent(),
         )
-
-        stopRecording()
     }
 
     private fun stopRecording() {

From a2dee2193afa8d451cff9b012ae33ad40bd0f2cc Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 24 Nov 2022 16:36:35 +0100
Subject: [PATCH 368/679] Fix bad condition

---
 .../usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
index b882d1625b..a401e8c157 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
@@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.transformWhile
-import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.RelationType
@@ -125,7 +125,7 @@ class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor(
                                         event.hasValue() && event.get().root.isRedacted().not()
                                     }
                                     .flatMapLatest { event ->
-                                        if (event.getOrNull()?.root?.isRedacted().orFalse()) {
+                                        if (event.getOrNull()?.root?.isRedacted().orTrue()) {
                                             // event is null or redacted, switch to the latest not redacted event
                                             getMostRecentRelatedEventFlow(room, voiceBroadcast)
                                         } else {

From d092c837745b48d588d38036fe4ff1d12368d2fa Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 24 Nov 2022 17:49:16 +0100
Subject: [PATCH 369/679] Fix wrong sequence number in stopped state event
 content

---
 .../recording/usecase/PauseVoiceBroadcastUseCase.kt          | 5 +++--
 .../recording/usecase/StopVoiceBroadcastUseCase.kt           | 5 +++--
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
index 817c1a72e4..0b22d7adf5 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt
@@ -54,7 +54,8 @@ class PauseVoiceBroadcastUseCase @Inject constructor(
     private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
         Timber.d("## PauseVoiceBroadcastUseCase: Send new voice broadcast info state event")
 
-        // immediately pause the recording
+        // save the last sequence number and immediately pause the recording
+        val lastSequence = voiceBroadcastRecorder?.currentSequence
         pauseRecording()
 
         room.stateService().sendStateEvent(
@@ -63,7 +64,7 @@ class PauseVoiceBroadcastUseCase @Inject constructor(
                 body = MessageVoiceBroadcastInfoContent(
                         relatesTo = reference,
                         voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value,
-                        lastChunkSequence = voiceBroadcastRecorder?.currentSequence,
+                        lastChunkSequence = lastSequence,
                 ).toContent(),
         )
     }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
index cd70671e76..b93bd346db 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt
@@ -55,7 +55,8 @@ class StopVoiceBroadcastUseCase @Inject constructor(
     private suspend fun stopVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
         Timber.d("## StopVoiceBroadcastUseCase: Send new voice broadcast info state event")
 
-        // Immediately stop the recording
+        // save the last sequence number and immediately stop the recording
+        val lastSequence = voiceBroadcastRecorder?.currentSequence
         stopRecording()
 
         room.stateService().sendStateEvent(
@@ -64,7 +65,7 @@ class StopVoiceBroadcastUseCase @Inject constructor(
                 body = MessageVoiceBroadcastInfoContent(
                         relatesTo = reference,
                         voiceBroadcastStateStr = VoiceBroadcastState.STOPPED.value,
-                        lastChunkSequence = voiceBroadcastRecorder?.currentSequence,
+                        lastChunkSequence = lastSequence,
                 ).toContent(),
         )
     }

From 9dba6d7c8c4e3497b3ef146d67a1edbae2c64788 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 24 Nov 2022 17:24:36 +0100
Subject: [PATCH 370/679] Fix issue on live playback detection

---
 .../listening/VoiceBroadcastPlayerImpl.kt      | 18 ++++++++----------
 1 file changed, 8 insertions(+), 10 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index f04b85859b..bd541d23e4 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -66,7 +66,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
     private var nextMediaPlayer: MediaPlayer? = null
     private var isPreparingNextPlayer: Boolean = false
 
-    private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
+    private var mostRecentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
 
     override var currentVoiceBroadcast: VoiceBroadcast? = null
     override var isLiveListening: Boolean = false
@@ -121,7 +121,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         // Clear playlist
         playlist.reset()
 
-        currentVoiceBroadcastEvent = null
+        mostRecentVoiceBroadcastEvent = null
         currentVoiceBroadcast = null
     }
 
@@ -159,7 +159,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         if (event == null) {
             stop()
         } else {
-            currentVoiceBroadcastEvent = event
+            mostRecentVoiceBroadcastEvent = event
             updateLiveListeningMode()
         }
     }
@@ -204,7 +204,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
 
         val playlistItem = when {
             position != null -> playlist.findByPosition(position)
-            currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
+            mostRecentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
             else -> playlist.firstOrNull()
         }
         val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
@@ -346,7 +346,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
     private fun updateLiveListeningMode(seekPosition: Int? = null) {
         isLiveListening = when {
             // the current voice broadcast is not live (ended)
-            currentVoiceBroadcastEvent?.isLive?.not().orFalse() -> false
+            mostRecentVoiceBroadcastEvent?.isLive != true -> false
             // the player is stopped or paused
             playingState == State.IDLE || playingState == State.PAUSED -> false
             seekPosition != null -> {
@@ -412,13 +412,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         override fun onCompletion(mp: MediaPlayer) {
             if (nextMediaPlayer != null) return
 
-            val content = currentVoiceBroadcastEvent?.content
-            val isLive = content?.isLive.orFalse()
-            if (!isLive && content?.lastChunkSequence == playlist.currentSequence) {
+            if (isLiveListening || mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
+                playingState = State.BUFFERING
+            } else {
                 // We'll not receive new chunks anymore so we can stop the live listening
                 stop()
-            } else {
-                playingState = State.BUFFERING
             }
         }
 

From 4427156f0b734649a9d253f7f5d37d81cc250e9d Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 25 Nov 2022 15:27:46 +0100
Subject: [PATCH 371/679] Restore trailing comma

---
 .../recording/usecase/ResumeVoiceBroadcastUseCase.kt            | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
index 5ad5b0704d..5be726c03e 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt
@@ -30,7 +30,7 @@ import timber.log.Timber
 import javax.inject.Inject
 
 class ResumeVoiceBroadcastUseCase @Inject constructor(
-        private val session: Session
+        private val session: Session,
 ) {
 
     suspend fun execute(roomId: String): Result = runCatching {

From aa53105f1713b237b06c059329175cc400784d5e Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 25 Nov 2022 15:32:29 +0100
Subject: [PATCH 372/679] improve flow stream

---
 ...MostRecentVoiceBroadcastStateEventUseCase.kt | 17 +++++++----------
 1 file changed, 7 insertions(+), 10 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
index a401e8c157..1b3ba6ba22 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
@@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.transformWhile
@@ -76,17 +75,15 @@ class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor(
                         // otherwise, observe most recent event changes
                         getMostRecentRelatedEventFlow(room, voiceBroadcast)
                                 .transformWhile { mostRecentEvent ->
-                                    emit(mostRecentEvent)
-                                    mostRecentEvent.hasValue()
-                                }
-                                .map {
-                                    if (!it.hasValue()) {
-                                        // no most recent event, fallback to started event
-                                        startedEvent
+                                    val hasValue = mostRecentEvent.hasValue()
+                                    if (hasValue) {
+                                        // keep the most recent event
+                                        emit(mostRecentEvent)
                                     } else {
-                                        // otherwise, keep the most recent event
-                                        it
+                                        // no most recent event, fallback to started event
+                                        emit(startedEvent)
                                     }
+                                    hasValue
                                 }
                     }
                 }

From 620bebc3a3f92f0f2bb0eadcd73c5468222a7f80 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 25 Nov 2022 15:35:52 +0100
Subject: [PATCH 373/679] Rewrite condition for better clarity

---
 .../usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
index 1b3ba6ba22..e0179e403f 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
@@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.transformWhile
-import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.RelationType
@@ -122,7 +121,8 @@ class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor(
                                         event.hasValue() && event.get().root.isRedacted().not()
                                     }
                                     .flatMapLatest { event ->
-                                        if (event.getOrNull()?.root?.isRedacted().orTrue()) {
+                                        val isRedactedOrNull = !event.hasValue() || event.get().root.isRedacted()
+                                        if (isRedactedOrNull) {
                                             // event is null or redacted, switch to the latest not redacted event
                                             getMostRecentRelatedEventFlow(room, voiceBroadcast)
                                         } else {

From 9840731778fe9947f8b0d7f8612e07448bd621b1 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Mon, 28 Nov 2022 16:15:07 +0100
Subject: [PATCH 374/679] Add todo for missing unit test

---
 .../recording/usecase/StartVoiceBroadcastUseCase.kt            | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
index 20f0615863..e3814608ea 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
@@ -85,7 +85,10 @@ class StartVoiceBroadcastUseCase @Inject constructor(
         )
 
         val voiceBroadcast = VoiceBroadcast(roomId = room.roomId, voiceBroadcastId = eventId)
+
+        // TODO Update unit test to cover the following line
         room.flow().liveTimelineEvent(eventId).unwrap().first() // wait for the event come back from the sync
+
         startRecording(room, voiceBroadcast, chunkLength, maxLength)
     }
 

From 4be954eeeb5cbc45b886347c0fb4e5e62768f618 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 24 Nov 2022 17:04:29 +0100
Subject: [PATCH 375/679] Voice Broadcast - Fix (live) playback stuck in
 buffering after receiving new chunk

---
 .../listening/VoiceBroadcastPlayerImpl.kt     | 90 +++++++++++--------
 1 file changed, 51 insertions(+), 39 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index bd541d23e4..f68e546809 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -36,7 +36,6 @@ import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
 import timber.log.Timber
@@ -73,7 +72,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         @MainThread
         set(value) {
             if (field != value) {
-                Timber.w("isLiveListening: $field -> $value")
+                Timber.w("## Voice Broadcast | isLiveListening: $field -> $value")
                 field = value
                 onLiveListeningChanged(value)
             }
@@ -83,7 +82,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         @MainThread
         set(value) {
             if (field != value) {
-                Timber.w("playingState: $field -> $value")
+                Timber.w("## Voice Broadcast | playingState: $field -> $value")
                 field = value
                 onPlayingStateChanged(value)
             }
@@ -175,41 +174,35 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
 
     private fun onPlaylistUpdated() {
         when (playingState) {
-            State.PLAYING -> {
-                if (nextMediaPlayer == null && !isPreparingNextPlayer) {
-                    prepareNextMediaPlayer()
-                }
-            }
+            State.PLAYING,
             State.PAUSED -> {
                 if (nextMediaPlayer == null && !isPreparingNextPlayer) {
                     prepareNextMediaPlayer()
                 }
             }
             State.BUFFERING -> {
-                val nextItem = playlist.getNextItem()
+                val nextItem = if (isLiveListening && playlist.currentSequence == null) {
+                    // live listening, jump to the last item if playback has not started
+                    playlist.lastOrNull()
+                } else {
+                    // not live or playback already started, request next item
+                    playlist.getNextItem()
+                }
                 if (nextItem != null) {
-                    val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
-                    startPlayback(savedPosition?.takeIf { it > 0 })
+                    startPlayback(nextItem.startTime)
                 }
             }
-            State.IDLE -> {
-                val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
-                startPlayback(savedPosition?.takeIf { it > 0 })
-            }
+            State.IDLE -> Unit // Should not happen
         }
     }
 
-    private fun startPlayback(position: Int? = null) {
+    private fun startPlayback(position: Int) {
         stopPlayer()
 
-        val playlistItem = when {
-            position != null -> playlist.findByPosition(position)
-            mostRecentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
-            else -> playlist.firstOrNull()
-        }
-        val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
-        val sequence = playlistItem.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return }
-        val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0
+        val playlistItem = playlist.findByPosition(position)
+        val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## Voice Broadcast | No content to play at position $position"); return }
+        val sequence = playlistItem.sequence ?: run { Timber.w("## Voice Broadcast | Playlist item has no sequence"); return }
+        val sequencePosition = position - playlistItem.startTime
         sessionScope.launch {
             try {
                 prepareMediaPlayer(content) { mp ->
@@ -223,7 +216,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
                     prepareNextMediaPlayer()
                 }
             } catch (failure: Throwable) {
-                Timber.e(failure, "Unable to start playback")
+                Timber.e(failure, "## Voice Broadcast | Unable to start playback: $failure")
                 throw VoiceFailure.UnableToPlay(failure)
             }
         }
@@ -248,8 +241,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
             currentMediaPlayer?.start()
             playingState = State.PLAYING
         } else {
-            val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
-            startPlayback(position)
+            val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
+            startPlayback(savedPosition)
         }
     }
 
@@ -274,9 +267,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
             isPreparingNextPlayer = true
             sessionScope.launch {
                 prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
-                    nextMediaPlayer = mp
-                    currentMediaPlayer?.setNextMediaPlayer(mp)
                     isPreparingNextPlayer = false
+                    nextMediaPlayer = mp
+                    when (playingState) {
+                        State.PLAYING,
+                        State.PAUSED -> {
+                            currentMediaPlayer?.setNextMediaPlayer(mp)
+                        }
+                        State.BUFFERING -> {
+                            mp.start()
+                            onNextMediaPlayerStarted(mp)
+                        }
+                        State.IDLE -> stopPlayer()
+                    }
                 }
             }
         }
@@ -287,7 +290,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         val audioFile = try {
             session.fileService().downloadFile(messageAudioContent)
         } catch (failure: Throwable) {
-            Timber.e(failure, "Unable to start playback")
+            Timber.e(failure, "Voice Broadcast | Download has failed: $failure")
             throw VoiceFailure.UnableToPlay(failure)
         }
 
@@ -375,6 +378,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         }
     }
 
+    private fun onNextMediaPlayerStarted(mp: MediaPlayer) {
+        playingState = State.PLAYING
+        playlist.currentSequence = playlist.currentSequence?.inc()
+        currentMediaPlayer = mp
+        nextMediaPlayer = null
+        prepareNextMediaPlayer()
+    }
+
     private fun getCurrentPlaybackPosition(): Int? {
         val playlistPosition = playlist.currentItem?.startTime
         val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
@@ -398,23 +409,24 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
 
         override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
             when (what) {
-                MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
-                    playlist.currentSequence = playlist.currentSequence?.inc()
-                    currentMediaPlayer = mp
-                    nextMediaPlayer = null
-                    playingState = State.PLAYING
-                    prepareNextMediaPlayer()
-                }
+                MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> onNextMediaPlayerStarted(mp)
             }
             return false
         }
 
         override fun onCompletion(mp: MediaPlayer) {
+            // Next media player is already attached to this player and will start playing automatically
             if (nextMediaPlayer != null) return
 
-            if (isLiveListening || mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
+            // Next media player is preparing but not attached yet, reset the currentMediaPlayer and let the new player take over
+            if (isPreparingNextPlayer) {
+                currentMediaPlayer?.release()
+                currentMediaPlayer = null
                 playingState = State.BUFFERING
-            } else {
+                return
+            }
+
+            if (!isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
                 // We'll not receive new chunks anymore so we can stop the live listening
                 stop()
             }

From c2d5908542d9a25f3b542d768f5defc8bf18f4ec Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 24 Nov 2022 18:10:36 +0100
Subject: [PATCH 376/679] Stop playback if live broadcast has ended and there
 is no more chunk to listen

---
 .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt     | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index f68e546809..addaaeec30 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -376,6 +376,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
             // Notify live mode change to all the listeners attached to the current voice broadcast id
             listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) }
         }
+
+        // Live has ended and last chunk has been reached, we can stop the playback
+        if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
+            stop()
+        }
     }
 
     private fun onNextMediaPlayerStarted(mp: MediaPlayer) {

From a4255525e009e99f844304ee64a68072fd654ce1 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 25 Nov 2022 15:57:33 +0100
Subject: [PATCH 377/679] Changelog

---
 changelog.d/7646.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7646.bugfix

diff --git a/changelog.d/7646.bugfix b/changelog.d/7646.bugfix
new file mode 100644
index 0000000000..7f771bc6f7
--- /dev/null
+++ b/changelog.d/7646.bugfix
@@ -0,0 +1 @@
+Voice Broadcast - Fix playback stuck in buffering mode

From 46fc0ac5637cdde1e7931dc3ecdfc513fb0aadbc Mon Sep 17 00:00:00 2001
From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com>
Date: Mon, 28 Nov 2022 17:29:30 +0100
Subject: [PATCH 378/679] ignore push for a thread if it's currently visible to
 user (#7641)

---
 changelog.d/7634.bugfix                       |  1 +
 .../app/core/pushers/VectorPushHandler.kt     | 13 +++--
 .../home/room/detail/TimelineFragment.kt      |  2 +
 .../notifications/NotifiableEventProcessor.kt | 10 +---
 .../notifications/NotifiableEventResolver.kt  |  5 +-
 .../notifications/NotifiableMessageEvent.kt   |  8 +++
 .../NotificationBroadcastReceiver.kt          |  1 +
 .../NotificationDrawerManager.kt              | 23 +++++++-
 .../notifications/NotificationEventQueue.kt   |  9 ++-
 .../NotifiableEventProcessorTest.kt           | 56 +++++++++++++++----
 .../test/fixtures/NotifiableEventFixture.kt   |  2 +
 11 files changed, 102 insertions(+), 28 deletions(-)
 create mode 100644 changelog.d/7634.bugfix

diff --git a/changelog.d/7634.bugfix b/changelog.d/7634.bugfix
new file mode 100644
index 0000000000..a3c829840a
--- /dev/null
+++ b/changelog.d/7634.bugfix
@@ -0,0 +1 @@
+Push notification for thread message is now shown correctly when user observes rooms main timeline
diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt
index b74028d579..0d2cd56995 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/VectorPushHandler.kt
@@ -28,6 +28,7 @@ import im.vector.app.core.network.WifiDetector
 import im.vector.app.core.pushers.model.PushData
 import im.vector.app.core.resources.BuildMeta
 import im.vector.app.features.notifications.NotifiableEventResolver
+import im.vector.app.features.notifications.NotifiableMessageEvent
 import im.vector.app.features.notifications.NotificationActionIds
 import im.vector.app.features.notifications.NotificationDrawerManager
 import im.vector.app.features.settings.VectorDataStore
@@ -142,11 +143,6 @@ class VectorPushHandler @Inject constructor(
         pushData.roomId ?: return
         pushData.eventId ?: return
 
-        // If the room is currently displayed, we will not show a notification, so no need to get the Event faster
-        if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(pushData.roomId)) {
-            return
-        }
-
         if (wifiDetector.isConnectedToWifi().not()) {
             Timber.tag(loggerTag.value).d("No WiFi network, do not get Event")
             return
@@ -157,6 +153,13 @@ class VectorPushHandler @Inject constructor(
 
         val resolvedEvent = notifiableEventResolver.resolveInMemoryEvent(session, event, canBeReplaced = true)
 
+        if (resolvedEvent is NotifiableMessageEvent) {
+            // If the room is currently displayed, we will not show a notification, so no need to get the Event faster
+            if (notificationDrawerManager.shouldIgnoreMessageEventInRoom(resolvedEvent)) {
+                return
+            }
+        }
+
         resolvedEvent
                 ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") }
                 ?.let {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 34d7e45028..b73d443832 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -972,6 +972,7 @@ class TimelineFragment :
     override fun onResume() {
         super.onResume()
         notificationDrawerManager.setCurrentRoom(timelineArgs.roomId)
+        notificationDrawerManager.setCurrentThread(timelineArgs.threadTimelineArgs?.rootThreadEventId)
         roomDetailPendingActionStore.data?.let { handlePendingAction(it) }
         roomDetailPendingActionStore.data = null
     }
@@ -991,6 +992,7 @@ class TimelineFragment :
     override fun onPause() {
         super.onPause()
         notificationDrawerManager.setCurrentRoom(null)
+        notificationDrawerManager.setCurrentThread(null)
     }
 
     private val emojiActivityResultLauncher = registerStartForActivityResult { activityResult ->
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt
index 6bdd2ab511..81b9844e36 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt
@@ -30,13 +30,13 @@ class NotifiableEventProcessor @Inject constructor(
         private val autoAcceptInvites: AutoAcceptInvites
 ) {
 
-    fun process(queuedEvents: List, currentRoomId: String?, renderedEvents: ProcessedEvents): ProcessedEvents {
+    fun process(queuedEvents: List, currentRoomId: String?, currentThreadId: String?, renderedEvents: ProcessedEvents): ProcessedEvents {
         val processedEvents = queuedEvents.map {
             val type = when (it) {
                 is InviteNotifiableEvent -> if (autoAcceptInvites.hideInvites) REMOVE else KEEP
                 is NotifiableMessageEvent -> when {
-                    shouldIgnoreMessageEventInRoom(currentRoomId, it.roomId) -> REMOVE
-                            .also { Timber.d("notification message removed due to currently viewing the same room") }
+                    it.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId) -> REMOVE
+                            .also { Timber.d("notification message removed due to currently viewing the same room or thread") }
                     outdatedDetector.isMessageOutdated(it) -> REMOVE
                             .also { Timber.d("notification message removed due to being read") }
                     else -> KEEP
@@ -55,8 +55,4 @@ class NotifiableEventProcessor @Inject constructor(
 
         return removedEventsDiff + processedEvents
     }
-
-    private fun shouldIgnoreMessageEventInRoom(currentRoomId: String?, roomId: String?): Boolean {
-        return currentRoomId != null && roomId == currentRoomId
-    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
index d28ab22684..988ab01ef8 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
 import org.matrix.android.sdk.api.session.events.model.isEdition
 import org.matrix.android.sdk.api.session.events.model.isImageMessage
 import org.matrix.android.sdk.api.session.events.model.supportsNotification
@@ -133,7 +134,7 @@ class NotifiableEventResolver @Inject constructor(
         }
     }
 
-    private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? {
+    private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableMessageEvent? {
         // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
         val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/)
 
@@ -155,6 +156,7 @@ class NotifiableEventResolver @Inject constructor(
                     body = body.toString(),
                     imageUriString = event.fetchImageIfPresent(session)?.toString(),
                     roomId = event.root.roomId!!,
+                    threadId = event.root.getRootThreadEventId(),
                     roomName = roomName,
                     matrixID = session.myUserId
             )
@@ -178,6 +180,7 @@ class NotifiableEventResolver @Inject constructor(
                             body = body,
                             imageUriString = event.fetchImageIfPresent(session)?.toString(),
                             roomId = event.root.roomId!!,
+                            threadId = event.root.getRootThreadEventId(),
                             roomName = roomName,
                             roomIsDirect = room.roomSummary()?.isDirect ?: false,
                             roomAvatarPath = session.contentUrlResolver()
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt
index 68268739a0..bbd8c6638c 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt
@@ -31,6 +31,7 @@ data class NotifiableMessageEvent(
         // NotSerializableException when persisting this to storage
         val imageUriString: String?,
         val roomId: String,
+        val threadId: String?,
         val roomName: String?,
         val roomIsDirect: Boolean = false,
         val roomAvatarPath: String? = null,
@@ -51,3 +52,10 @@ data class NotifiableMessageEvent(
     val imageUri: Uri?
         get() = imageUriString?.let { Uri.parse(it) }
 }
+
+fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom(currentRoomId: String?, currentThreadId: String?): Boolean {
+    return when (currentRoomId) {
+        null -> false
+        else -> roomId == currentRoomId && threadId == currentThreadId
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
index 180351f806..455f4778e8 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt
@@ -148,6 +148,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
                 body = message,
                 imageUriString = null,
                 roomId = room.roomId,
+                threadId = null, // needs to be changed: https://github.com/vector-im/element-android/issues/7475
                 roomName = room.roomSummary()?.displayName ?: room.roomId,
                 roomIsDirect = room.roomSummary()?.isDirect == true,
                 outGoingMessage = true,
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
index 2623045cf3..4f05e83bd4 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
@@ -63,6 +63,7 @@ class NotificationDrawerManager @Inject constructor(
     private val notificationState by lazy { createInitialNotificationState() }
     private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
     private var currentRoomId: String? = null
+    private var currentThreadId: String? = null
     private val firstThrottler = FirstThrottler(200)
 
     private var useCompleteNotificationFormat = vectorPreferences.useCompleteNotificationFormat()
@@ -123,6 +124,22 @@ class NotificationDrawerManager @Inject constructor(
         }
     }
 
+    /**
+     * Should be called when the application is currently opened and showing timeline for the given threadId.
+     * Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room.
+     */
+    fun setCurrentThread(threadId: String?) {
+        updateEvents {
+            val hasChanged = threadId != currentThreadId
+            currentThreadId = threadId
+            currentRoomId?.let { roomId ->
+                if (hasChanged && threadId != null) {
+                    it.clearMessagesForThread(roomId, threadId)
+                }
+            }
+        }
+    }
+
     fun notificationStyleChanged() {
         updateEvents {
             val newSettings = vectorPreferences.useCompleteNotificationFormat()
@@ -164,7 +181,7 @@ class NotificationDrawerManager @Inject constructor(
     private fun refreshNotificationDrawerBg() {
         Timber.v("refreshNotificationDrawerBg()")
         val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
-            notifiableEventProcessor.process(queuedEvents.rawEvents(), currentRoomId, renderedEvents).also {
+            notifiableEventProcessor.process(queuedEvents.rawEvents(), currentRoomId, currentThreadId, renderedEvents).also {
                 queuedEvents.clearAndAdd(it.onlyKeptEvents())
             }
         }
@@ -198,8 +215,8 @@ class NotificationDrawerManager @Inject constructor(
         notificationRenderer.render(session.myUserId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, eventsToRender)
     }
 
-    fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
-        return currentRoomId != null && roomId == currentRoomId
+    fun shouldIgnoreMessageEventInRoom(resolvedEvent: NotifiableMessageEvent): Boolean {
+        return resolvedEvent.shouldIgnoreMessageEventInRoom(currentRoomId, currentThreadId)
     }
 
     companion object {
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt
index f02424803a..8aff9c3bf2 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt
@@ -122,15 +122,20 @@ data class NotificationEventQueue(
     }
 
     fun clearMemberShipNotificationForRoom(roomId: String) {
-        Timber.v("clearMemberShipOfRoom $roomId")
+        Timber.d("clearMemberShipOfRoom $roomId")
         queue.removeAll { it is InviteNotifiableEvent && it.roomId == roomId }
     }
 
     fun clearMessagesForRoom(roomId: String) {
-        Timber.v("clearMessageEventOfRoom $roomId")
+        Timber.d("clearMessageEventOfRoom $roomId")
         queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId }
     }
 
+    fun clearMessagesForThread(roomId: String, threadId: String) {
+        Timber.d("clearMessageEventOfThread $roomId, $threadId")
+        queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId && it.threadId == threadId }
+    }
+
     fun rawEvents(): List = queue
 }
 
diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt
index 131a423316..59e42a9568 100644
--- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt
+++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt
@@ -27,6 +27,7 @@ import org.junit.Test
 import org.matrix.android.sdk.api.session.events.model.EventType
 
 private val NOT_VIEWING_A_ROOM: String? = null
+private val NOT_VIEWING_A_THREAD: String? = null
 
 class NotifiableEventProcessorTest {
 
@@ -42,7 +43,7 @@ class NotifiableEventProcessorTest {
                 aSimpleNotifiableEvent(eventId = "event-2")
         )
 
-        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
+        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, currentThreadId = NOT_VIEWING_A_THREAD, renderedEvents = emptyList())
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.KEEP to events[0],
@@ -54,7 +55,7 @@ class NotifiableEventProcessorTest {
     fun `given redacted simple event when processing then remove redaction event`() {
         val events = listOf(aSimpleNotifiableEvent(eventId = "event-1", type = EventType.REDACTION))
 
-        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
+        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, currentThreadId = NOT_VIEWING_A_THREAD, renderedEvents = emptyList())
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.REMOVE to events[0]
@@ -69,7 +70,7 @@ class NotifiableEventProcessorTest {
                 anInviteNotifiableEvent(roomId = "room-2")
         )
 
-        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
+        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, currentThreadId = NOT_VIEWING_A_THREAD, renderedEvents = emptyList())
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.REMOVE to events[0],
@@ -85,7 +86,7 @@ class NotifiableEventProcessorTest {
                 anInviteNotifiableEvent(roomId = "room-2")
         )
 
-        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
+        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, currentThreadId = NOT_VIEWING_A_THREAD, renderedEvents = emptyList())
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.KEEP to events[0],
@@ -98,7 +99,7 @@ class NotifiableEventProcessorTest {
         val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1"))
         outdatedDetector.givenEventIsOutOfDate(events[0])
 
-        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
+        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, currentThreadId = NOT_VIEWING_A_THREAD, renderedEvents = emptyList())
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.REMOVE to events[0],
@@ -110,7 +111,7 @@ class NotifiableEventProcessorTest {
         val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1"))
         outdatedDetector.givenEventIsInDate(events[0])
 
-        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
+        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, currentThreadId = NOT_VIEWING_A_THREAD, renderedEvents = emptyList())
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.KEEP to events[0],
@@ -118,16 +119,51 @@ class NotifiableEventProcessorTest {
     }
 
     @Test
-    fun `given viewing the same room as message event when processing then removes message`() {
-        val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1"))
+    fun `given viewing the same room main timeline when processing main timeline message event then removes message`() {
+        val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1", threadId = null))
+
+        val result = eventProcessor.process(events, currentRoomId = "room-1", currentThreadId = null, renderedEvents = emptyList())
+
+        result shouldBeEqualTo listOfProcessedEvents(
+                Type.REMOVE to events[0],
+        )
+    }
 
-        val result = eventProcessor.process(events, currentRoomId = "room-1", renderedEvents = emptyList())
+    @Test
+    fun `given viewing the same thread timeline when processing thread message event then removes message`() {
+        val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1", threadId = "thread-1"))
+
+        val result = eventProcessor.process(events, currentRoomId = "room-1", currentThreadId = "thread-1", renderedEvents = emptyList())
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.REMOVE to events[0],
         )
     }
 
+    @Test
+    fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() {
+        val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1", threadId = "thread-1"))
+        outdatedDetector.givenEventIsInDate(events[0])
+
+        val result = eventProcessor.process(events, currentRoomId = "room-1", currentThreadId = null, renderedEvents = emptyList())
+
+        result shouldBeEqualTo listOfProcessedEvents(
+                Type.KEEP to events[0],
+        )
+    }
+
+    @Test
+    fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() {
+        val events = listOf(aNotifiableMessageEvent(eventId = "event-1", roomId = "room-1"))
+        outdatedDetector.givenEventIsInDate(events[0])
+
+        val result = eventProcessor.process(events, currentRoomId = "room-1", currentThreadId = "thread-1", renderedEvents = emptyList())
+
+        result shouldBeEqualTo listOfProcessedEvents(
+                Type.KEEP to events[0],
+        )
+    }
+
     @Test
     fun `given events are different to rendered events when processing then removes difference`() {
         val events = listOf(aSimpleNotifiableEvent(eventId = "event-1"))
@@ -136,7 +172,7 @@ class NotifiableEventProcessorTest {
                 ProcessedEvent(Type.KEEP, anInviteNotifiableEvent(roomId = "event-2"))
         )
 
-        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, renderedEvents = renderedEvents)
+        val result = eventProcessor.process(events, currentRoomId = NOT_VIEWING_A_ROOM, currentThreadId = NOT_VIEWING_A_THREAD, renderedEvents = renderedEvents)
 
         result shouldBeEqualTo listOfProcessedEvents(
                 Type.REMOVE to renderedEvents[1].event,
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt
index 397ca80f84..a6d21a46c9 100644
--- a/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt
+++ b/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt
@@ -63,6 +63,7 @@ fun anInviteNotifiableEvent(
 fun aNotifiableMessageEvent(
         eventId: String = "a-message-event-id",
         roomId: String = "a-message-room-id",
+        threadId: String? = null,
         isRedacted: Boolean = false
 ) = NotifiableMessageEvent(
         eventId = eventId,
@@ -73,6 +74,7 @@ fun aNotifiableMessageEvent(
         senderId = "sending-id",
         body = "message-body",
         roomId = roomId,
+        threadId = threadId,
         roomName = "room-name",
         roomIsDirect = false,
         canBeReplaced = false,

From fe0bca75f8d40040206971f7f432571a2d95825b Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 00:59:45 +0100
Subject: [PATCH 379/679] Change log level

---
 .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index addaaeec30..724be600a3 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -72,7 +72,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         @MainThread
         set(value) {
             if (field != value) {
-                Timber.w("## Voice Broadcast | isLiveListening: $field -> $value")
+                Timber.d("## Voice Broadcast | isLiveListening: $field -> $value")
                 field = value
                 onLiveListeningChanged(value)
             }
@@ -82,7 +82,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         @MainThread
         set(value) {
             if (field != value) {
-                Timber.w("## Voice Broadcast | playingState: $field -> $value")
+                Timber.d("## Voice Broadcast | playingState: $field -> $value")
                 field = value
                 onPlayingStateChanged(value)
             }

From 912de8286ff3958302cbb4e976b3284d5812cc5b Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 00:17:38 +0100
Subject: [PATCH 380/679] Move buffering view in tile header

---
 .../src/main/res/values/strings.xml           |  3 +-
 .../MessageVoiceBroadcastListeningItem.kt     | 15 ++++----
 .../views/VoiceBroadcastBufferingView.kt      | 37 +++++++++++++++++++
 ...e_event_voice_broadcast_listening_stub.xml | 19 ++++------
 .../layout/view_voice_broadcast_buffering.xml | 26 +++++++++++++
 5 files changed, 79 insertions(+), 21 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastBufferingView.kt
 create mode 100644 vector/src/main/res/layout/view_voice_broadcast_buffering.xml

diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index f1d5bfbcad..616acfb343 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3092,12 +3092,13 @@
     (%1$s)
 
     Live
+    
+    Buffering…
     Resume voice broadcast record
     Pause voice broadcast record
     Stop voice broadcast record
     Play or resume voice broadcast
     Pause voice broadcast
-    Buffering
     Fast backward 30 seconds
     Fast forward 30 seconds
     Can’t start a new voice broadcast
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
index e5cb677763..8d32875f0c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -17,7 +17,6 @@
 package im.vector.app.features.home.room.detail.timeline.item
 
 import android.text.format.DateUtils
-import android.view.View
 import android.widget.ImageButton
 import android.widget.SeekBar
 import android.widget.TextView
@@ -30,6 +29,7 @@ import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAc
 import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
 import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.views.VoiceBroadcastBufferingView
 import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
 
 @EpoxyModelClass
@@ -63,10 +63,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
             playPauseButton.setOnClickListener {
                 if (player.currentVoiceBroadcast == voiceBroadcast) {
                     when (player.playingState) {
-                        VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
+                        VoiceBroadcastPlayer.State.PLAYING,
+                        VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
                         VoiceBroadcastPlayer.State.PAUSED,
                         VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
-                        VoiceBroadcastPlayer.State.BUFFERING -> Unit
                     }
                 } else {
                     callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
@@ -86,7 +86,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
     override fun renderMetadata(holder: Holder) {
         with(holder) {
             broadcasterNameMetadata.value = recorderName
-            voiceBroadcastMetadata.isVisible = true
             listenersCountMetadata.isVisible = false
         }
     }
@@ -102,10 +101,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
     private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
         with(holder) {
             bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
-            playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
+            voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
 
             when (state) {
-                VoiceBroadcastPlayer.State.PLAYING -> {
+                VoiceBroadcastPlayer.State.PLAYING,
+                VoiceBroadcastPlayer.State.BUFFERING -> {
                     playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
                     playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
                 }
@@ -114,7 +114,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
                     playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
                     playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
                 }
-                VoiceBroadcastPlayer.State.BUFFERING -> Unit
             }
 
             renderLiveIndicator(holder)
@@ -174,7 +173,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
 
     class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
         val playPauseButton by bind(R.id.playPauseButton)
-        val bufferingView by bind(R.id.bufferingView)
+        val bufferingView by bind(R.id.bufferingMetadata)
         val fastBackwardButton by bind(R.id.fastBackwardButton)
         val fastForwardButton by bind(R.id.fastForwardButton)
         val seekBar by bind(R.id.seekBar)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastBufferingView.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastBufferingView.kt
new file mode 100644
index 0000000000..eabefa323e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastBufferingView.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.voicebroadcast.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import im.vector.app.databinding.ViewVoiceBroadcastBufferingBinding
+
+class VoiceBroadcastBufferingView @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+    init {
+        ViewVoiceBroadcastBufferingBinding.inflate(
+                LayoutInflater.from(context),
+                this
+        )
+    }
+}
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index 1d31afba99..f872db3d00 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -51,7 +51,7 @@
         android:layout_height="wrap_content"
         android:layout_marginTop="4dp"
         android:orientation="vertical"
-        app:constraint_referenced_ids="broadcasterNameMetadata,voiceBroadcastMetadata,listenersCountMetadata"
+        app:constraint_referenced_ids="broadcasterNameMetadata,bufferingMetadata,voiceBroadcastMetadata,listenersCountMetadata"
         app:flow_horizontalAlign="start"
         app:flow_verticalGap="4dp"
         app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
@@ -64,6 +64,11 @@
         app:metadataIcon="@drawable/ic_voice_broadcast_mic"
         tools:metadataValue="@sample/users.json/data/displayName" />
 
+    
+
     
 
@@ -117,16 +122,6 @@
         android:src="@drawable/ic_play_pause_play"
         app:tint="?vctr_content_secondary" />
 
-    
-
     
+
+
+    
+
+    
+

From 12a86e7d29e4e1a9efc12e1daf438f972b1c6c8c Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 00:21:38 +0100
Subject: [PATCH 381/679] Reduce tiles padding

---
 .../item_timeline_event_voice_broadcast_listening_stub.xml      | 2 +-
 .../item_timeline_event_voice_broadcast_recording_stub.xml      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index f872db3d00..f26a6bf7ab 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -7,7 +7,7 @@
     android:layout_height="wrap_content"
     android:background="@drawable/rounded_rect_shape_8"
     android:backgroundTint="?vctr_content_quinary"
-    android:padding="@dimen/layout_vertical_margin">
+    android:padding="12dp">
 
     
+    android:padding="12dp">
 
     
Date: Tue, 29 Nov 2022 00:24:29 +0100
Subject: [PATCH 382/679] Remove seekBar padding

---
 .../item_timeline_event_voice_broadcast_listening_stub.xml     | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index f26a6bf7ab..d3c0dc4dc5 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -138,6 +138,9 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginTop="24dp"
+        android:layout_marginEnd="6dp"
+        android:paddingStart="0dp"
+        android:paddingEnd="0dp"
         android:progressDrawable="@drawable/bg_seek_bar"
         android:thumbTint="?vctr_content_tertiary"
         app:layout_constraintBottom_toBottomOf="parent"

From 9458276a4e62af6331492dc69570cb6cf16fcaa3 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 00:36:02 +0100
Subject: [PATCH 383/679] Change seekBar and duration colors to secondary

---
 vector/src/main/res/drawable/bg_seek_bar.xml                | 4 ++--
 .../src/main/res/layout/item_timeline_event_audio_stub.xml  | 6 +++---
 .../item_timeline_event_voice_broadcast_listening_stub.xml  | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/vector/src/main/res/drawable/bg_seek_bar.xml b/vector/src/main/res/drawable/bg_seek_bar.xml
index 0a33522dfd..eff461091e 100644
--- a/vector/src/main/res/drawable/bg_seek_bar.xml
+++ b/vector/src/main/res/drawable/bg_seek_bar.xml
@@ -13,9 +13,9 @@
             
                 
             
         
     
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml
index 2a6fbf5a9e..4c4286af9b 100644
--- a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml
@@ -73,7 +73,7 @@
             android:layout_marginTop="12dp"
             android:layout_marginBottom="10dp"
             android:progressDrawable="@drawable/bg_seek_bar"
-            android:thumbTint="?vctr_content_tertiary"
+            android:thumbTint="?vctr_content_secondary"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toStartOf="@id/audioPlaybackTime"
             app:layout_constraintTop_toBottomOf="@id/audioPlaybackControlButton"
@@ -85,7 +85,7 @@
             style="@style/Widget.Vector.TextView.Caption"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textColor="?vctr_content_tertiary"
+            android:textColor="?vctr_content_secondary"
             android:layout_marginEnd="4dp"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="@id/audioSeekBar"
@@ -104,4 +104,4 @@
         android:visibility="gone"
         tools:visibility="visible" />
 
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index d3c0dc4dc5..e002102e6c 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -142,7 +142,7 @@
         android:paddingStart="0dp"
         android:paddingEnd="0dp"
         android:progressDrawable="@drawable/bg_seek_bar"
-        android:thumbTint="?vctr_content_tertiary"
+        android:thumbTint="?vctr_content_secondary"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toStartOf="@id/playbackDuration"
         app:layout_constraintStart_toStartOf="parent"

From 7b4c1650332cdd2650f5f064320b65a2b1a8a0b8 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 01:22:31 +0100
Subject: [PATCH 384/679] Changelog

---
 changelog.d/7655.wip | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7655.wip

diff --git a/changelog.d/7655.wip b/changelog.d/7655.wip
new file mode 100644
index 0000000000..24358007a9
--- /dev/null
+++ b/changelog.d/7655.wip
@@ -0,0 +1 @@
+Voice Broadcast - Update the buffering display in the timeline

From 471bf853c81bf4877ec6274dd6a6e2e1bdce2106 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 01:41:31 +0100
Subject: [PATCH 385/679] Remove voice broadcast chunks from the room
 attachments list

---
 .../detail/timeline/helper/TimelineEventVisibilityHelper.kt   | 3 +--
 .../app/features/roomprofile/uploads/RoomUploadsViewModel.kt  | 4 ++++
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index 1360151074..382f1c2301 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -22,7 +22,6 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
 import im.vector.app.features.voicebroadcast.isVoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
@@ -257,7 +256,7 @@ class TimelineEventVisibilityHelper @Inject constructor(
             return true
         }
 
-        if (root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse()) {
+        if (root.asMessageAudioEvent().isVoiceBroadcast()) {
             return true
         }
 
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
index 87dff2f00b..a71490f4a7 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt
@@ -26,10 +26,12 @@ import dagger.assisted.AssistedInject
 import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
 import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
 import org.matrix.android.sdk.flow.flow
 import org.matrix.android.sdk.flow.unwrap
 
@@ -78,6 +80,8 @@ class RoomUploadsViewModel @AssistedInject constructor(
                 token = result.nextToken
 
                 val groupedUploadEvents = result.uploadEvents
+                        // Remove voice broadcast chunks from the attachments
+                        .filterNot { it.root.asMessageAudioEvent().isVoiceBroadcast() }
                         .groupBy {
                             it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_IMAGE ||
                                     it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_VIDEO

From f8881638f9a18cd0a8d3622eb6313e7cab70ceeb Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 01:49:50 +0100
Subject: [PATCH 386/679] Changelog

---
 changelog.d/7656.wip | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7656.wip

diff --git a/changelog.d/7656.wip b/changelog.d/7656.wip
new file mode 100644
index 0000000000..ab0e47289f
--- /dev/null
+++ b/changelog.d/7656.wip
@@ -0,0 +1 @@
+Voice Broadcast - Remove voice messages related to a VB from the room attachments

From 9ab2d1afb0e31d8b5f1a1612bc75deec8b9395b2 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 10:50:31 +0100
Subject: [PATCH 387/679] Fix thumb cropped

---
 .../item_timeline_event_voice_broadcast_listening_stub.xml   | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index e002102e6c..3c59d49418 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -7,6 +7,8 @@
     android:layout_height="wrap_content"
     android:background="@drawable/rounded_rect_shape_8"
     android:backgroundTint="?vctr_content_quinary"
+    android:clipChildren="false"
+    android:clipToPadding="false"
     android:padding="12dp">
 
     
+        tools:progress="0" />
 
     
Date: Tue, 29 Nov 2022 10:54:31 +0100
Subject: [PATCH 388/679] Fix playback not in buffering if waiting for new
 chunks

---
 .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt        | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index 724be600a3..95a4ddcf2e 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -434,6 +434,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
             if (!isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
                 // We'll not receive new chunks anymore so we can stop the live listening
                 stop()
+            } else {
+                playingState = State.BUFFERING
             }
         }
 

From 42b3ecc0b69985cdca753ac16a50d31a8240c76e Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 14:12:39 +0100
Subject: [PATCH 389/679] Fix pause/resume playback not working correctly

---
 .../im/vector/app/core/extensions/Flow.kt     | 35 ++++++++++++++
 .../detail/composer/AudioMessageHelper.kt     |  2 +-
 .../helper/AudioMessagePlaybackTracker.kt     | 25 +++++-----
 .../MessageVoiceBroadcastListeningItem.kt     |  4 +-
 .../listening/VoiceBroadcastPlayerImpl.kt     | 47 +++++++++----------
 5 files changed, 74 insertions(+), 39 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/core/extensions/Flow.kt

diff --git a/vector/src/main/java/im/vector/app/core/extensions/Flow.kt b/vector/src/main/java/im/vector/app/core/extensions/Flow.kt
new file mode 100644
index 0000000000..82e6e5f9a6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/extensions/Flow.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.extensions
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/**
+ * Returns a flow that invokes the given action after the first value of the upstream flow is emitted downstream.
+ */
+fun  Flow.onFirst(action: (T) -> Unit): Flow = flow {
+    var emitted = false
+    collect { value ->
+        emit(value) // always emit value
+
+        if (!emitted) {
+            action(value) // execute the action after the first emission
+            emitted = true
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
index b5ea528bd7..900de041d0 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
@@ -149,7 +149,7 @@ class AudioMessageHelper @Inject constructor(
     }
 
     private fun startPlayback(id: String, file: File) {
-        val currentPlaybackTime = playbackTracker.getPlaybackTime(id)
+        val currentPlaybackTime = playbackTracker.getPlaybackTime(id) ?: 0
 
         try {
             FileInputStream(file).use { fis ->
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
index 90fd66f9ab..c34cbbc74a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
@@ -67,8 +67,8 @@ class AudioMessagePlaybackTracker @Inject constructor() {
     }
 
     fun startPlayback(id: String) {
-        val currentPlaybackTime = getPlaybackTime(id)
-        val currentPercentage = getPercentage(id)
+        val currentPlaybackTime = getPlaybackTime(id) ?: 0
+        val currentPercentage = getPercentage(id) ?: 0f
         val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage)
         setState(id, currentState)
         // Pause any active playback
@@ -85,9 +85,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
     }
 
     fun pausePlayback(id: String) {
-        if (getPlaybackState(id) is Listener.State.Playing) {
-            val currentPlaybackTime = getPlaybackTime(id)
-            val currentPercentage = getPercentage(id)
+        val state = getPlaybackState(id)
+        if (state is Listener.State.Playing) {
+            val currentPlaybackTime = state.playbackTime
+            val currentPercentage = state.percentage
             setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage))
         }
     }
@@ -110,21 +111,23 @@ class AudioMessagePlaybackTracker @Inject constructor() {
 
     fun getPlaybackState(id: String) = states[id]
 
-    fun getPlaybackTime(id: String): Int {
+    fun getPlaybackTime(id: String): Int? {
         return when (val state = states[id]) {
             is Listener.State.Playing -> state.playbackTime
             is Listener.State.Paused -> state.playbackTime
-            /* Listener.State.Idle, */
-            else -> 0
+            is Listener.State.Recording,
+            Listener.State.Idle,
+            null -> null
         }
     }
 
-    fun getPercentage(id: String): Float {
+    fun getPercentage(id: String): Float? {
         return when (val state = states[id]) {
             is Listener.State.Playing -> state.percentage
             is Listener.State.Paused -> state.percentage
-            /* Listener.State.Idle, */
-            else -> 0f
+            is Listener.State.Recording,
+            Listener.State.Idle,
+            null -> null
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
index 8d32875f0c..38fe1e8f17 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -141,14 +141,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
             renderBackwardForwardButtons(holder, playbackState)
             renderLiveIndicator(holder)
             if (!isUserSeeking) {
-                holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
+                holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
             }
         }
     }
 
     private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
         val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused
-        val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
+        val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
         val canBackward = isPlayingOrPaused && playbackTime > 0
         val canForward = isPlayingOrPaused && playbackTime < duration
         holder.fastBackwardButton.isInvisible = !canBackward
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index 95a4ddcf2e..f8025d078e 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -21,6 +21,7 @@ import android.media.MediaPlayer
 import android.media.MediaPlayer.OnPreparedListener
 import androidx.annotation.MainThread
 import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.extensions.onFirst
 import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
 import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.voice.VoiceFailure
@@ -145,11 +146,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         playingState = State.BUFFERING
 
         observeVoiceBroadcastStateEvent(voiceBroadcast)
-        fetchPlaylistAndStartPlayback(voiceBroadcast)
     }
 
     private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) {
         voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
+                .onFirst { fetchPlaylistAndStartPlayback(voiceBroadcast) }
                 .onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) }
                 .launchIn(sessionScope)
     }
@@ -222,24 +223,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
         }
     }
 
-    private fun pausePlayback(positionMillis: Int? = null) {
-        if (positionMillis == null) {
+    private fun pausePlayback() {
+        playingState = State.PAUSED // This will trigger a playing state update and save the current position
+        if (currentMediaPlayer != null) {
             currentMediaPlayer?.pause()
         } else {
             stopPlayer()
-            val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId
-            val duration = playlist.duration.takeIf { it > 0 }
-            if (voiceBroadcastId != null && duration != null) {
-                playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
-            }
         }
-        playingState = State.PAUSED
     }
 
     private fun resumePlayback() {
         if (currentMediaPlayer != null) {
-            currentMediaPlayer?.start()
             playingState = State.PLAYING
+            currentMediaPlayer?.start()
         } else {
             val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
             startPlayback(savedPosition)
@@ -256,7 +252,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
                 startPlayback(positionMillis)
             }
             playingState == State.IDLE || playingState == State.PAUSED -> {
-                pausePlayback(positionMillis)
+                stopPlayer()
+                playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
             }
         }
     }
@@ -366,8 +363,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
                     isLiveListening && newSequence == playlist.currentSequence
                 }
             }
-            // otherwise, stay in live or go in live if we reached the latest sequence
-            else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence
+            // if there is no saved position, go in live
+            getCurrentPlaybackPosition() == null -> true
+            // if we reached the latest sequence, go in live
+            playlist.currentSequence == playlist.lastOrNull()?.sequence -> true
+            // otherwise, do not change
+            else -> isLiveListening
         }
     }
 
@@ -392,9 +393,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
     }
 
     private fun getCurrentPlaybackPosition(): Int? {
-        val playlistPosition = playlist.currentItem?.startTime
-        val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
-        val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
+        val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null
+        val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) }
+        val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId)
         return computedPosition ?: savedPosition
     }
 
@@ -423,19 +424,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
             // Next media player is already attached to this player and will start playing automatically
             if (nextMediaPlayer != null) return
 
-            // Next media player is preparing but not attached yet, reset the currentMediaPlayer and let the new player take over
-            if (isPreparingNextPlayer) {
-                currentMediaPlayer?.release()
-                currentMediaPlayer = null
-                playingState = State.BUFFERING
-                return
-            }
-
-            if (!isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
+            val hasEnded = !isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence
+            if (hasEnded) {
                 // We'll not receive new chunks anymore so we can stop the live listening
                 stop()
             } else {
+                // Enter in buffering mode and release current media player
                 playingState = State.BUFFERING
+                currentMediaPlayer?.release()
+                currentMediaPlayer = null
             }
         }
 

From 1415504f84b2296865e536de9fdf0509e8e95bd2 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 29 Nov 2022 14:32:05 +0100
Subject: [PATCH 390/679] Rename view ids

---
 .../voicebroadcast/views/VoiceBroadcastMetadataView.kt      | 6 +++---
 .../src/main/res/layout/view_voice_broadcast_buffering.xml  | 5 ++---
 .../src/main/res/layout/view_voice_broadcast_metadata.xml   | 2 +-
 3 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
index e142cb15ce..c743d8a542 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
@@ -37,9 +37,9 @@ class VoiceBroadcastMetadataView @JvmOverloads constructor(
     )
 
     var value: String
-        get() = views.metadataValue.text.toString()
+        get() = views.metadataText.text.toString()
         set(newValue) {
-            views.metadataValue.text = newValue
+            views.metadataText.text = newValue
         }
 
     init {
@@ -61,6 +61,6 @@ class VoiceBroadcastMetadataView @JvmOverloads constructor(
 
     private fun setValue(typedArray: TypedArray) {
         val value = typedArray.getString(R.styleable.VoiceBroadcastMetadataView_metadataValue)
-        views.metadataValue.text = value
+        views.metadataText.text = value
     }
 }
diff --git a/vector/src/main/res/layout/view_voice_broadcast_buffering.xml b/vector/src/main/res/layout/view_voice_broadcast_buffering.xml
index c58858f033..e292169537 100644
--- a/vector/src/main/res/layout/view_voice_broadcast_buffering.xml
+++ b/vector/src/main/res/layout/view_voice_broadcast_buffering.xml
@@ -8,17 +8,16 @@
     tools:parentTag="android.widget.LinearLayout">
 
     
 
     
 
     
Date: Tue, 29 Nov 2022 15:07:51 +0000
Subject: [PATCH 391/679] Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/
---
 library/ui-strings/src/main/res/values-pt-rBR/strings.xml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
index 8baba5df53..8129a234fb 100644
--- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
+++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml
@@ -2723,7 +2723,7 @@
     Sessões não-verificadas
     Sessões inativas são sessões que você não tem usado em algum tempo, mas elas continuam a receber chaves de encriptação.
 \n
-\nRemover sessões inativas melhora segurança e performance, e torna-o mais fácil para você identificar se uma nova sessão é suspeita.
+\nRemover sessões inativas melhora segurança e performance, e torna mais fácil para você identificar se uma nova sessão é suspeita.
     Sessões inativas
     Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica.
     Nomes de sessões personalizadas podem ajudar você a reconhecer seus dispositivos mais facilmente.
@@ -2844,9 +2844,9 @@
     Não dá pra começar um novo broadcast de voz
     Avançar rápido 30 segundos
     Retroceder 30 segundos
-    Sessões verificadas são onde quer que você está usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada.
+    Sessões verificadas são onde quer que você esteja usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada.
 \n
-\nIsto significa que você tem todas as chaves necessitadas para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.
+\nIsto significa que você tem todas as chaves necessárias para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.
     
         Fazer signout de %1$d sessão
         Fazer signout de %1$d sessões

From 1c0fe56329f828aab086dba0f814b5cadc71f11f Mon Sep 17 00:00:00 2001
From: LinAGKar 
Date: Mon, 28 Nov 2022 21:32:51 +0000
Subject: [PATCH 392/679] Translated using Weblate (Swedish)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/
---
 .../ui-strings/src/main/res/values-sv/strings.xml  | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml
index 65318096c7..45cfe4338b 100644
--- a/library/ui-strings/src/main/res/values-sv/strings.xml
+++ b/library/ui-strings/src/main/res/values-sv/strings.xml
@@ -2852,4 +2852,18 @@
     Kan inte starta en ny röstsändning
     Spola framåt 30 sekunder
     Spola tillbaka 30 sekunder
+    skickade en omröstning.
+    skickade en dekal.
+    skickade en video.
+    skickade en bild.
+    skickade ett röstmeddelande.
+    skickade en ljudfil.
+    skickade en fil.
+    Svar på
+    Dölj IP-adress
+    Visa IP-adress
+    %1$s kvar
+    Citerar
+    Besvarar %s
+    Redigerar
 
\ No newline at end of file

From b3ffc4d76cf09f3cdeb688a17ad4c08ba7fc86d5 Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk 
Date: Mon, 28 Nov 2022 01:47:14 +0000
Subject: [PATCH 393/679] Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2556 of 2556 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/
---
 .../ui-strings/src/main/res/values-uk/strings.xml  | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml
index 19889892ff..d65a7a1da7 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -2966,16 +2966,16 @@
     Вийти
     Залишилося %1$s
     надсилає аудіофайл.
-    відправив файл.
+    надсилає файл.
     У відповідь на
     Сховати IP-адресу
-    створив голосування.
-    відправив наліпку.
-    відправив відео.
-    відправив зображення.
-    відправив голосове повідомлення.
+    створює опитування.
+    надсилає наліпку.
+    надсилає відео.
+    надсилає зображення.
+    надсилає голосове повідомлення.
     Показати IP-адресу
     Цитуючи
-    У відповідь на %s
+    У відповідь %s
     Редагування
 
\ No newline at end of file

From 4a70ea851814ddb394ecd091b620bc1062323e2a Mon Sep 17 00:00:00 2001
From: LinAGKar 
Date: Mon, 28 Nov 2022 21:24:35 +0000
Subject: [PATCH 394/679] Translated using Weblate (Swedish)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sv/
---
 fastlane/metadata/android/sv-SE/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/sv-SE/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105080.txt b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt
new file mode 100644
index 0000000000..cee589ed35
--- /dev/null
+++ b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Huvudsakliga ändringar i den här versionen: buggfixar och förbättringar.
+Full ändringslogg: https://github.com/vector-im/element-android/releases

From 31a1b09e3498e03615be6d94cbdabda08d6b26d5 Mon Sep 17 00:00:00 2001
From: jonnyandrew 
Date: Wed, 30 Nov 2022 09:00:37 +0000
Subject: [PATCH 395/679] [Rich text editor] Fix design and spacing of rich
 text editor (#7658)

Improve design and spacing of the rich text editor.

Minor changes to
 - position of input field relative to buttons
 - spacing around attachments button
 - spacing around send button
 - selectable backgrounds
---
 7658.bugfix                                   |  1 +
 .../detail/composer/RichTextComposerLayout.kt |  4 ++--
 .../res/layout/composer_rich_text_layout.xml  | 22 +++++++++----------
 3 files changed, 14 insertions(+), 13 deletions(-)
 create mode 100644 7658.bugfix

diff --git a/7658.bugfix b/7658.bugfix
new file mode 100644
index 0000000000..a5ab85b191
--- /dev/null
+++ b/7658.bugfix
@@ -0,0 +1 @@
+[Rich text editor] Fix design and spacing of rich text editor
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index 4664c4213c..a058d4fdaa 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -274,8 +274,8 @@ class RichTextComposerLayout @JvmOverloads constructor(
                 connect(R.id.composerEditTextOuterBorder, ConstraintSet.START, R.id.composerLayoutContent, ConstraintSet.START, dpToPx(12))
                 connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.composerLayoutContent, ConstraintSet.END, dpToPx(12))
             } else {
-                connect(R.id.composerEditTextOuterBorder, ConstraintSet.TOP, R.id.composerLayoutContent, ConstraintSet.TOP, dpToPx(10))
-                connect(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM, R.id.composerLayoutContent, ConstraintSet.BOTTOM, dpToPx(10))
+                connect(R.id.composerEditTextOuterBorder, ConstraintSet.TOP, R.id.composerLayoutContent, ConstraintSet.TOP, dpToPx(8))
+                connect(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM, R.id.composerLayoutContent, ConstraintSet.BOTTOM, dpToPx(8))
                 connect(R.id.composerEditTextOuterBorder, ConstraintSet.START, R.id.attachmentButton, ConstraintSet.END, 0)
                 connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.sendButton, ConstraintSet.START, 0)
             }
diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml
index 88f96c528e..3484616c72 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout.xml
@@ -32,26 +32,26 @@
 
         
 
+        
         
 
         
Date: Wed, 30 Nov 2022 09:54:16 +0100
Subject: [PATCH 396/679] Release script: Fix script error

---
 tools/release/releaseScript.sh | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index 943e9771f2..e1579b00fc 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -77,7 +77,7 @@ fi
 cp ./vector-app/build.gradle ./vector-app/build.gradle.bak
 sed "s/ext.versionMajor = .*/ext.versionMajor = ${versionMajor}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
 sed "s/ext.versionMinor = .*/ext.versionMinor = ${versionMinor}/" ./vector-app/build.gradle     > ./vector-app/build.gradle.bak
-sed "s/ext.versionPatch = .*/ext.versionPatch = ${patchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
+sed "s/ext.versionPatch = .*/ext.versionPatch = ${versionPatch}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
 rm ./vector-app/build.gradle.bak
 cp ./matrix-sdk-android/build.gradle ./matrix-sdk-android/build.gradle.bak
 sed "s/\"SDK_VERSION\", .*$/\"SDK_VERSION\", \"\\\\\"${version}\\\\\"\"/" ./matrix-sdk-android/build.gradle.bak > ./matrix-sdk-android/build.gradle
@@ -184,7 +184,6 @@ git checkout develop
 # Set next version
 printf "\n================================================================================\n"
 printf "Setting next version on file './vector-app/build.gradle'...\n"
-nextPatchVersion=$((versionPatch + 2))
 cp ./vector-app/build.gradle ./vector-app/build.gradle.bak
 sed "s/ext.versionPatch = .*/ext.versionPatch = ${nextPatchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
 rm ./vector-app/build.gradle.bak

From b0029f2dd3409a568cb7d56f9c1fddfcb19bef1c Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 10:00:12 +0100
Subject: [PATCH 397/679] Release script: Check if git flow is enabled

---
 tools/release/releaseScript.sh | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index e1579b00fc..de97f2e7bb 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -38,6 +38,17 @@ if [[ ! -f ${releaseScriptFullPath} ]]; then
   exit 1
 fi
 
+# Check if git flow is enabled
+git flow config >/dev/null 2>&1
+if [[ $? == 0 ]]
+then
+    printf "Git flow is initialized"
+else
+    printf "Git flow is not initialized. Initializing...\n"
+    # All default value, just set 'v' for tag prefix
+    git flow init -d -t 'v'
+fi
+
 # Guessing version to propose a default version
 versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut  -d " " -f3`
 versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut  -d " " -f3`

From b58050f4969c867e8f7ce93924c6372e18fba151 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 30 Nov 2022 09:13:47 +0000
Subject: [PATCH 398/679] Bump kotlin-reflect from 1.7.21 to 1.7.22 (#7665)

Bumps [kotlin-reflect](https://github.com/JetBrains/kotlin) from 1.7.21 to 1.7.22.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.21...v1.7.22)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin:kotlin-reflect
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 vector-app/build.gradle | 2 +-
 vector/build.gradle     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index 86b94a8497..be67e5ff43 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -402,7 +402,7 @@ dependencies {
     androidTestImplementation libs.mockk.mockkAndroid
     androidTestUtil libs.androidx.orchestrator
     androidTestImplementation libs.androidx.fragmentTesting
-    androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"
+    androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
     debugImplementation libs.androidx.fragmentTesting
     debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
 }
diff --git a/vector/build.gradle b/vector/build.gradle
index 890236422e..1a5179f07b 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -331,5 +331,5 @@ dependencies {
     androidTestImplementation libs.mockk.mockkAndroid
     androidTestUtil libs.androidx.orchestrator
     debugImplementation libs.androidx.fragmentTesting
-    androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"
+    androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
 }

From a73fe9585f0ddcc73370d7caced947e628a28072 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 30 Nov 2022 09:15:18 +0000
Subject: [PATCH 399/679] Bump danger/danger-js from 11.1.4 to 11.2.0 (#7584)

Bumps [danger/danger-js](https://github.com/danger/danger-js) from 11.1.4 to 11.2.0.
- [Release notes](https://github.com/danger/danger-js/releases)
- [Changelog](https://github.com/danger/danger-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/danger/danger-js/compare/11.1.4...11.2.0)

---
updated-dependencies:
- dependency-name: danger/danger-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/danger.yml  | 2 +-
 .github/workflows/quality.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 30b6600c94..e5226d0723 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -11,7 +11,7 @@ jobs:
       - run: |
           npm install --save-dev @babel/plugin-transform-flow-strip-types
       - name: Danger
-        uses: danger/danger-js@11.1.4
+        uses: danger/danger-js@11.2.0
         with:
           args: "--dangerfile tools/danger/dangerfile.js"
         env:
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 9d9e8e76e8..57dd5a6a45 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -66,7 +66,7 @@ jobs:
           yarn add danger-plugin-lint-report --dev
       - name: Danger lint
         if: always()
-        uses: danger/danger-js@11.1.4
+        uses: danger/danger-js@11.2.0
         with:
           args: "--dangerfile tools/danger/dangerfile-lint.js"
         env:

From eb7154d42c03c6358558af33f864d3119b041775 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 10:25:21 +0100
Subject: [PATCH 400/679] Remove the obsolete description of the attribute.

---
 .github/ISSUE_TEMPLATE/release.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml
index 0c3542997a..4ab77af5a0 100644
--- a/.github/ISSUE_TEMPLATE/release.yml
+++ b/.github/ISSUE_TEMPLATE/release.yml
@@ -10,7 +10,6 @@ body:
     id: checklist
     attributes:
       label: Release checklist
-      description: For the template example, we are releasing the version 1.2.3. Replace 1.2.3 with the version in the issue body.
       placeholder: |
         If you are reading this, you have deleted the content of the release template: undo the deletion or start again.
       value: |

From d447d809f733e5a7154d1a922196afe548c83959 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 10:46:58 +0100
Subject: [PATCH 401/679] Changelog for version 1.5.10

---
 CHANGES.md               | 36 ++++++++++++++++++++++++++++++++++++
 changelog.d/2725.feature |  1 -
 changelog.d/5679.bugfix  |  1 -
 changelog.d/6996.sdk     |  1 -
 changelog.d/7546.feature |  1 -
 changelog.d/7555.bugfix  |  1 -
 changelog.d/7577.feature |  1 -
 changelog.d/7583.misc    |  1 -
 changelog.d/7594.misc    |  1 -
 changelog.d/7604.bugfix  |  1 -
 changelog.d/7620.bugfix  |  1 -
 changelog.d/7626.sdk     |  2 --
 changelog.d/7629.wip     |  1 -
 changelog.d/7634.bugfix  |  1 -
 changelog.d/7646.bugfix  |  1 -
 changelog.d/7655.wip     |  1 -
 changelog.d/7656.wip     |  1 -
 17 files changed, 36 insertions(+), 17 deletions(-)
 delete mode 100644 changelog.d/2725.feature
 delete mode 100644 changelog.d/5679.bugfix
 delete mode 100644 changelog.d/6996.sdk
 delete mode 100644 changelog.d/7546.feature
 delete mode 100644 changelog.d/7555.bugfix
 delete mode 100644 changelog.d/7577.feature
 delete mode 100644 changelog.d/7583.misc
 delete mode 100644 changelog.d/7594.misc
 delete mode 100644 changelog.d/7604.bugfix
 delete mode 100644 changelog.d/7620.bugfix
 delete mode 100644 changelog.d/7626.sdk
 delete mode 100644 changelog.d/7629.wip
 delete mode 100644 changelog.d/7634.bugfix
 delete mode 100644 changelog.d/7646.bugfix
 delete mode 100644 changelog.d/7655.wip
 delete mode 100644 changelog.d/7656.wip

diff --git a/CHANGES.md b/CHANGES.md
index 442d3641dd..022591f5a1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,39 @@
+Changes in Element v1.5.10 (2022-11-30)
+=======================================
+
+Features ✨
+----------
+ - Add setting to allow disabling direct share ([#2725](https://github.com/vector-im/element-android/issues/2725))
+ - [Device Manager] Toggle IP address visibility ([#7546](https://github.com/vector-im/element-android/issues/7546))
+ - New implementation of the full screen mode for the Rich Text Editor. ([#7577](https://github.com/vector-im/element-android/issues/7577))
+
+Bugfixes 🐛
+----------
+ - Fix italic text is truncated when bubble mode and markdown is enabled ([#5679](https://github.com/vector-im/element-android/issues/5679))
+ - Missing translations on "replyTo" messages ([#7555](https://github.com/vector-im/element-android/issues/7555))
+ - ANR on session start when sending client info is enabled ([#7604](https://github.com/vector-im/element-android/issues/7604))
+ - Make the plain text mode layout of the RTE more compact. ([#7620](https://github.com/vector-im/element-android/issues/7620))
+ - Push notification for thread message is now shown correctly when user observes rooms main timeline ([#7634](https://github.com/vector-im/element-android/issues/7634))
+ - Voice Broadcast - Fix playback stuck in buffering mode ([#7646](https://github.com/vector-im/element-android/issues/7646))
+
+In development 🚧
+----------------
+ - Voice Broadcast - Handle redaction of the state events on the listener and recorder sides ([#7629](https://github.com/vector-im/element-android/issues/7629))
+ - Voice Broadcast - Update the buffering display in the timeline ([#7655](https://github.com/vector-im/element-android/issues/7655))
+ - Voice Broadcast - Remove voice messages related to a VB from the room attachments ([#7656](https://github.com/vector-im/element-android/issues/7656))
+
+SDK API changes ⚠️
+------------------
+ - Added support for read receipts in threads. Now user in a room can have multiple read receipts (one per thread + one in main thread + one without threadId) ([#6996](https://github.com/vector-im/element-android/issues/6996))
+ - Sync Filter now taking in account homeserver capabilities to not pass unsupported parameters.
+  Sync Filter is now configured by providing SyncFilterBuilder class instance, instead of Filter to identify Filter changes related to homeserver capabilities ([#7626](https://github.com/vector-im/element-android/issues/7626))
+
+Other changes
+-------------
+ - Remove usage of Buildkite. ([#7583](https://github.com/vector-im/element-android/issues/7583))
+ - Better validation of edits ([#7594](https://github.com/vector-im/element-android/issues/7594))
+
+
 Changes in Element v1.5.8 (2022-11-17)
 ======================================
 
diff --git a/changelog.d/2725.feature b/changelog.d/2725.feature
deleted file mode 100644
index eb3fcaed57..0000000000
--- a/changelog.d/2725.feature
+++ /dev/null
@@ -1 +0,0 @@
-Add setting to allow disabling direct share
diff --git a/changelog.d/5679.bugfix b/changelog.d/5679.bugfix
deleted file mode 100644
index 0394bc3e5d..0000000000
--- a/changelog.d/5679.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix italic text is truncated when bubble mode and markdown is enabled
diff --git a/changelog.d/6996.sdk b/changelog.d/6996.sdk
deleted file mode 100644
index 588ec160d7..0000000000
--- a/changelog.d/6996.sdk
+++ /dev/null
@@ -1 +0,0 @@
-Added support for read receipts in threads. Now user in a room can have multiple read receipts (one per thread + one in main thread + one without threadId)
diff --git a/changelog.d/7546.feature b/changelog.d/7546.feature
deleted file mode 100644
index 94450082c9..0000000000
--- a/changelog.d/7546.feature
+++ /dev/null
@@ -1 +0,0 @@
-[Device Manager] Toggle IP address visibility
diff --git a/changelog.d/7555.bugfix b/changelog.d/7555.bugfix
deleted file mode 100644
index 064b21a9e5..0000000000
--- a/changelog.d/7555.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Missing translations on "replyTo" messages
diff --git a/changelog.d/7577.feature b/changelog.d/7577.feature
deleted file mode 100644
index e21ccb13c0..0000000000
--- a/changelog.d/7577.feature
+++ /dev/null
@@ -1 +0,0 @@
-New implementation of the full screen mode for the Rich Text Editor.
diff --git a/changelog.d/7583.misc b/changelog.d/7583.misc
deleted file mode 100644
index 3c63aeaadf..0000000000
--- a/changelog.d/7583.misc
+++ /dev/null
@@ -1 +0,0 @@
-Remove usage of Buildkite.
diff --git a/changelog.d/7594.misc b/changelog.d/7594.misc
deleted file mode 100644
index 5c5771d8d0..0000000000
--- a/changelog.d/7594.misc
+++ /dev/null
@@ -1 +0,0 @@
-Better validation of edits
diff --git a/changelog.d/7604.bugfix b/changelog.d/7604.bugfix
deleted file mode 100644
index 0fbee55bce..0000000000
--- a/changelog.d/7604.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-ANR on session start when sending client info is enabled
diff --git a/changelog.d/7620.bugfix b/changelog.d/7620.bugfix
deleted file mode 100644
index 55c0e423ad..0000000000
--- a/changelog.d/7620.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Make the plain text mode layout of the RTE more compact.
diff --git a/changelog.d/7626.sdk b/changelog.d/7626.sdk
deleted file mode 100644
index 4d9f28183a..0000000000
--- a/changelog.d/7626.sdk
+++ /dev/null
@@ -1,2 +0,0 @@
-Sync Filter now taking in account homeserver capabilities to not pass unsupported parameters.
-Sync Filter is now configured by providing SyncFilterBuilder class instance, instead of Filter to identify Filter changes related to homeserver capabilities
diff --git a/changelog.d/7629.wip b/changelog.d/7629.wip
deleted file mode 100644
index ecc4449b6f..0000000000
--- a/changelog.d/7629.wip
+++ /dev/null
@@ -1 +0,0 @@
-Voice Broadcast - Handle redaction of the state events on the listener and recorder sides
diff --git a/changelog.d/7634.bugfix b/changelog.d/7634.bugfix
deleted file mode 100644
index a3c829840a..0000000000
--- a/changelog.d/7634.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Push notification for thread message is now shown correctly when user observes rooms main timeline
diff --git a/changelog.d/7646.bugfix b/changelog.d/7646.bugfix
deleted file mode 100644
index 7f771bc6f7..0000000000
--- a/changelog.d/7646.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Voice Broadcast - Fix playback stuck in buffering mode
diff --git a/changelog.d/7655.wip b/changelog.d/7655.wip
deleted file mode 100644
index 24358007a9..0000000000
--- a/changelog.d/7655.wip
+++ /dev/null
@@ -1 +0,0 @@
-Voice Broadcast - Update the buffering display in the timeline
diff --git a/changelog.d/7656.wip b/changelog.d/7656.wip
deleted file mode 100644
index ab0e47289f..0000000000
--- a/changelog.d/7656.wip
+++ /dev/null
@@ -1 +0,0 @@
-Voice Broadcast - Remove voice messages related to a VB from the room attachments

From a656b003296ac0bf72485e7a37d4345153586f23 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 10:50:06 +0100
Subject: [PATCH 402/679] Adding fastlane file for version 1.5.10

---
 fastlane/metadata/android/en-US/changelogs/40105100.txt | 2 ++
 tools/release/releaseScript.sh                          | 1 +
 2 files changed, 3 insertions(+)
 create mode 100644 fastlane/metadata/android/en-US/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/en-US/changelogs/40105100.txt b/fastlane/metadata/android/en-US/changelogs/40105100.txt
new file mode 100644
index 0000000000..c9e5ba5fa9
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Main changes in this version: New implementation of the full screen mode for the Rich Text Editor and bugfixes.
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index de97f2e7bb..d8980b9da7 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -166,6 +166,7 @@ fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}"
 printf "Main changes in this version: TODO.\nFull changelog: https://github.com/vector-im/element-android/releases" > ${fastlanePathFile}
 
 read -p "I have created the file ${fastlanePathFile}, please edit it and press enter when it's done."
+git add ${fastlanePathFile}
 git commit -a -m "Adding fastlane file for version ${version}"
 
 printf "\n================================================================================\n"

From 52477aa9d57527b707680716c33b9c230ecf1a39 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 11:03:58 +0100
Subject: [PATCH 403/679] version++

---
 matrix-sdk-android/build.gradle | 2 +-
 vector-app/build.gradle         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 60b0329fbc..4be8d55614 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -62,7 +62,7 @@ android {
         // that the app's state is completely cleared between tests.
         testInstrumentationRunnerArguments clearPackageData: 'true'
 
-        buildConfigField "String", "SDK_VERSION", "\"1.5.10\""
+        buildConfigField "String", "SDK_VERSION", "\"1.5.12\""
 
         buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
         buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index be67e5ff43..fb80010d17 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -37,7 +37,7 @@ ext.versionMinor = 5
 // Note: even values are reserved for regular release, odd values for hotfix release.
 // When creating a hotfix, you should decrease the value, since the current value
 // is the value for the next regular release.
-ext.versionPatch = 10
+ext.versionPatch = 12
 
 static def getGitTimestamp() {
     def cmd = 'git show -s --format=%ct'

From 8cf8852aae5d9ab500395a5572dc1976c3165e56 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 11:50:27 +0100
Subject: [PATCH 404/679] Release script: Update last part of the script

---
 tools/release/releaseScript.sh | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index d8980b9da7..3abe8a46f1 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -225,12 +225,13 @@ else
 fi
 
 printf "\n================================================================================\n"
-read -p "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch. Press enter when it's done."
+printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n"
+read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl
 
 printf "\n================================================================================\n"
 printf "Running the release script...\n"
 cd ${releaseScriptLocation}
-${releaseScriptFullPath} "v${version}"
+${releaseScriptFullPath} "v${version}" ${artifactUrl}
 cd -
 
 printf "\n================================================================================\n"

From 714f8b3a7512cb82c07c655eae1baa3775d577a5 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 12:01:30 +0100
Subject: [PATCH 405/679] Release script: Improve release script again.

---
 tools/release/releaseScript.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index 3abe8a46f1..fa5afc6faa 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -235,8 +235,8 @@ ${releaseScriptFullPath} "v${version}" ${artifactUrl}
 cd -
 
 printf "\n================================================================================\n"
+read -p "Installing apk on a real device, press enter when a real device is connected. "
 apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk"
-printf "Installing apk on a real device...\n"
 adb -d install ${apkPath}
 
 read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done."

From 0868686caadf3551a3ac0e42e19db1601ef3b748 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 30 Nov 2022 12:52:39 +0100
Subject: [PATCH 406/679] Release script: Send message to Android Room

---
 tools/release/releaseScript.sh | 23 ++++++++++++++++++++---
 1 file changed, 20 insertions(+), 3 deletions(-)

diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index fa5afc6faa..83572d4910 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -246,9 +246,26 @@ read -p "Create the release on gitHub from the tag https://github.com/vector-im/
 read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done."
 
 printf "\n================================================================================\n"
-printf "Ping the Android Internal room. Here is an example of message which can be sent:\n\n"
-printf "@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!\n\n"
-read -p "Press enter when it's done."
+printf "Message for the Android internal room:\n\n"
+message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
+printf "${message}\n\n"
+
+matrixOrgToken="${MATRIX_ORG_TOKEN}"
+if [[ -z "${matrixOrgToken}" ]]; then
+  read -p "MATRIX_ORG_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done "
+else
+  read -p "Send this message to the room (yes/no) default to yes? " doSend
+  doSend=${doSend:-yes}
+  if [ ${doSend} == "yes" ]; then
+    printf "Sending message...\n"
+    transactionId=`openssl rand -hex 16`
+    # Element Android internal
+    matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org"
+    curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${matrixOrgToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId}
+  else
+    printf "Message not sent, please send it manually!\n"
+  fi
+fi
 
 printf "\n================================================================================\n"
 printf "Congratulation! Kudos for using this script! Have a nice day!\n"

From b699e9db3a71b4aa0182e41895a99b2c82831d19 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 30 Nov 2022 13:58:28 +0100
Subject: [PATCH 407/679] Bump sentry-android from 6.7.0 to 6.9.0 (#7668)

Bumps [sentry-android](https://github.com/getsentry/sentry-java) from 6.7.0 to 6.9.0.
- [Release notes](https://github.com/getsentry/sentry-java/releases)
- [Changelog](https://github.com/getsentry/sentry-java/blob/6.9.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-java/compare/6.7.0...6.9.0)

---
updated-dependencies:
- dependency-name: io.sentry:sentry-android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index 31c32bb26b..51f8f9df0d 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -26,7 +26,7 @@ def jjwt = "0.11.5"
 // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
 // the whole commit which set version 0.16.0-SNAPSHOT
 def vanniktechEmoji = "0.16.0-SNAPSHOT"
-def sentry = "6.7.0"
+def sentry = "6.9.0"
 def fragment = "1.5.4"
 // Testing
 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819

From d1cef1bc5c56fc804fdfb9059d489ca43dbbe13f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 30 Nov 2022 23:02:20 +0000
Subject: [PATCH 408/679] Bump com.autonomousapps.dependency-analysis from
 1.16.0 to 1.17.0

Bumps com.autonomousapps.dependency-analysis from 1.16.0 to 1.17.0.

---
updated-dependencies:
- dependency-name: com.autonomousapps.dependency-analysis
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 51604b67a8..58084ab64d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -48,7 +48,7 @@ plugins {
     id "com.google.devtools.ksp" version "1.7.21-1.0.8"
 
     // Dependency Analysis
-    id 'com.autonomousapps.dependency-analysis' version "1.16.0"
+    id 'com.autonomousapps.dependency-analysis' version "1.17.0"
     // Gradle doctor
     id "com.osacky.doctor" version "0.8.1"
 }

From e4212bd7dbcfdbb4c99ba237cc7e6c91937920e2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 30 Nov 2022 23:03:13 +0000
Subject: [PATCH 409/679] Bump flipper from 0.174.0 to 0.175.0

Bumps `flipper` from 0.174.0 to 0.175.0.

Updates `flipper` from 0.174.0 to 0.175.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.174.0...v0.175.0)

Updates `flipper-network-plugin` from 0.174.0 to 0.175.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.174.0...v0.175.0)

---
updated-dependencies:
- dependency-name: com.facebook.flipper:flipper
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.facebook.flipper:flipper-network-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index 51f8f9df0d..cb10170e93 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -17,7 +17,7 @@ def markwon = "4.6.2"
 def moshi = "1.14.0"
 def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
-def flipper = "0.174.0"
+def flipper = "0.175.0"
 def epoxy = "5.0.0"
 def mavericks = "3.0.1"
 def glide = "4.14.2"

From 279756bdfb64ab1c1df07be594313d0ae3ae334d Mon Sep 17 00:00:00 2001
From: waclaw66 
Date: Thu, 1 Dec 2022 05:59:21 +0000
Subject: [PATCH 410/679] Translated using Weblate (Czech)

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/
---
 library/ui-strings/src/main/res/values-cs/strings.xml | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml
index f260a129fc..67cc3353aa 100644
--- a/library/ui-strings/src/main/res/values-cs/strings.xml
+++ b/library/ui-strings/src/main/res/values-cs/strings.xml
@@ -2789,7 +2789,7 @@
     Pravost této šifrované zprávy nelze v tomto zařízení zaručit.
     Požadujte, aby klávesnice neaktualizovala žádné personalizované údaje, jako je historie psaní a slovník, na základě toho, co jste napsali v konverzacích. Upozorňujeme, že některé klávesnice nemusí toto nastavení respektovat.
     Inkognito klávesnice
-    Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu obyčejného textu
+    Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu prostého textu
     Hlasové vysílání
     Otevřít nástroje pro vývojáře
     🔒 V nastavení zabezpečení jste povolili šifrování pouze do ověřených relací pro všechny místnosti.
@@ -2824,8 +2824,8 @@
     ${app_name} potřebuje oprávnění k zobrazování oznámení. Oznámení mohou zobrazovat vaše zprávy, pozvánky atd.
 \n
 \nPro zobrazování oznámení povolte přístup na dalších vyskakovacích oknech.
-    Vyzkoušejte rozšířený textový editor (textový režim již brzy)
-    Povolit rozšířený textový editor
+    Vyzkoušejte editor formátovaného textu (režim prostého textu již brzy)
+    Povolit editor formátovaného textu
     Ujistěte se, že znáte původ tohoto kódu. Propojením zařízení poskytnete někomu plný přístup ke svému účtu.
     Potvrdit
     Zkuste to znovu
@@ -2868,7 +2868,7 @@
     Druhé zařízení je již přihlášeno.
     Při nastavování zabezpečeného zasílání zpráv se vyskytl problém se zabezpečením. Může být napadena jedna z následujících věcí: váš domovský server; vaše internetové připojení; vaše zařízení;
     Žádost se nezdařila.
-    Ukládání do vyrovnávací paměti
+    Ukládání do vyrovnávací paměti…
     Pozastavit hlasové vysílání
     Přehrát nebo obnovit hlasové vysílání
     Ukončit záznam hlasového vysílání
@@ -2922,4 +2922,6 @@
     Citace
     Odpovídám na %s
     Úpravy
+    Zobrazit poslední chaty v nabídce sdílení systému
+    Povolit přímé sdílení
 
\ No newline at end of file

From a0528fe0ce203f5ac1bfe4f012a636daa0536bb8 Mon Sep 17 00:00:00 2001
From: Vri 
Date: Wed, 30 Nov 2022 06:31:56 +0000
Subject: [PATCH 411/679] Translated using Weblate (German)

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
---
 library/ui-strings/src/main/res/values-de/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml
index be53c15026..809ee477fc 100644
--- a/library/ui-strings/src/main/res/values-de/strings.xml
+++ b/library/ui-strings/src/main/res/values-de/strings.xml
@@ -2815,7 +2815,7 @@
     Die Anfrage ist fehlgeschlagen.
     Abspielen oder fortsetzen der Sprachübertragung
     Fortsetzen der Sprachübertragung
-    Puffere
+    Puffere …
     Pausiere Sprachübertragung
     Stoppe Aufzeichnung der Sprachübertragung
     Pausiere Aufzeichnung der Sprachübertragung
@@ -2865,4 +2865,6 @@
     %s antworten
     IP-Adresse ausblenden
     IP-Adresse anzeigen
+    Kürzliche Unterhaltungen im Teilen-Menü des Systems anzeigen
+    Direktes Teilen aktivieren
 
\ No newline at end of file

From c6ed280a6f0a65b09cb63a8638afd3205fd2ccaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= 
Date: Wed, 30 Nov 2022 08:14:33 +0000
Subject: [PATCH 412/679] Translated using Weblate (Estonian)

Currently translated at 99.6% (2550 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/et/
---
 library/ui-strings/src/main/res/values-et/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml
index 156221379d..96d9650ceb 100644
--- a/library/ui-strings/src/main/res/values-et/strings.xml
+++ b/library/ui-strings/src/main/res/values-et/strings.xml
@@ -2805,7 +2805,7 @@
     Teine seade on juba võrku loginud.
     Turvalise sõnumivahetuse ülesseadmisel tekkis turvaviga. Üks kolmest võib olla sattunud vale osapoole kontrolli alla: sinu koduserver, sinu internetiühendus või sinu seade;
     Päring ei õnnestunud.
-    Andmed on puhverdamisel
+    Andmed on puhverdamisel…
     Alusta või jätka ringhäälingukõne esitamist
     Lõpeta ringhäälingukõne salvestamine
     Peata ringhäälingukõne salvestamine
@@ -2857,4 +2857,6 @@
     saatis video.
     saatis kleepsu.
     koostas küsitluse.
+    Kasuta otsejagamist
+    Näita viimaseid vestlusi süsteemses jagamisvaates
 
\ No newline at end of file

From 27acb198ab9c760e4ec348c153abc9a088dd1f0d Mon Sep 17 00:00:00 2001
From: Danial Behzadi 
Date: Wed, 30 Nov 2022 11:24:07 +0000
Subject: [PATCH 413/679] Translated using Weblate (Persian)

Currently translated at 99.7% (2551 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/
---
 library/ui-strings/src/main/res/values-fa/strings.xml | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml
index cc8d60a87b..a3a74df10f 100644
--- a/library/ui-strings/src/main/res/values-fa/strings.xml
+++ b/library/ui-strings/src/main/res/values-fa/strings.xml
@@ -943,7 +943,7 @@
 \n
 \nپیام‌هایتان با قفل‌هایی امن شده‌اند و فقط شما و گیرندگان دیگر، کلیدهای یکتا را برای قفل‌گشاییشان دارید.
     امنیت
-    بثیش‌تر بدانید
+    بیش‌تر بدانید
     بیش‌تر
     کنش‌های مدیر
     تنظمیات اتاق
@@ -2783,7 +2783,7 @@
     نظرسنجی‌ها
     پیوست‌ها
     برچسب‌ها
-    میانگیری
+    میانگیری…
     زنده
     تأیید
     ۳
@@ -2844,4 +2844,9 @@
     نقل کردن
     پاسخ دادن به %s
     ویرایش کردن
+    می‌توانید با یک رمز QR از این افزاره برای ورود به افزاره‌ای همراه یا روی وب استفاده کنید. دو راه برای این کار وجود دارد:
+    مشکلی امنیتی در برپایی پیام‌رسانی امن وجود داشت. ممکن است یکی از موارد زیر دستکاری شده باشند: کارساز خانیگیتان؛ اتّصال اینترنتیتان؛ افزاره(های)تان؛
+    لطفاً مطمئن شوید که مبدأ این کد را می‌دانید. با پیوند دادن افزاره‌ها، دسترسی کامل را به حسابتان می‌دهید.
+    نمایش گپ‌های اخیر در فهرست هم رسانی سامانه
+    به کار انداختن هم‌رسانی مستقیم
 
\ No newline at end of file

From 1c7f789928f3414be104f934cdad76c25a9cd25b Mon Sep 17 00:00:00 2001
From: Glandos 
Date: Wed, 30 Nov 2022 11:55:22 +0000
Subject: [PATCH 414/679] Translated using Weblate (French)

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/
---
 library/ui-strings/src/main/res/values-fr/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml
index cf49733bdf..d74d3bac71 100644
--- a/library/ui-strings/src/main/res/values-fr/strings.xml
+++ b/library/ui-strings/src/main/res/values-fr/strings.xml
@@ -2814,7 +2814,7 @@
     Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire :
     Se connecter avec un QR code
     Scanner le QR code
-    Mise en mémoire tampon
+    Mise en mémoire tampon…
     Mettre en pause la diffusion audio
     Lire ou continuer la diffusion audio
     Arrêter l’enregistrement de la diffusion audio
@@ -2866,4 +2866,6 @@
     Citation de
     Réponse à %s
     Modification
+    Affiche les conversations récentes dans le menu de partage du système
+    Activer le partage direct
 
\ No newline at end of file

From ab1db4de6870537774b8153dd82d28cc5153bca4 Mon Sep 17 00:00:00 2001
From: Linerly 
Date: Tue, 29 Nov 2022 23:32:26 +0000
Subject: [PATCH 415/679] Translated using Weblate (Indonesian)

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/id/
---
 library/ui-strings/src/main/res/values-in/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml
index ce9e524067..da4c474689 100644
--- a/library/ui-strings/src/main/res/values-in/strings.xml
+++ b/library/ui-strings/src/main/res/values-in/strings.xml
@@ -2762,7 +2762,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
     Permintaan gagal.
     Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan.
     Aktifkan siaran suara (dalam pengembangan aktif)
-    Memuat
+    Memuat…
     Jeda siaran suara
     Mainkan atau lanjutkan siaran suara
     Hentikan rekaman siaran suara
@@ -2812,4 +2812,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.
     Mengedit
     Tampilkan alamat IP
     Membalas ke %s
+    Tampilkan obrolan terkini dalam menu pembagian sistem
+    Aktifkan pembagian langsung
 
\ No newline at end of file

From 05e6a59a867664d9a35c3d5927ed22bced059a79 Mon Sep 17 00:00:00 2001
From: random 
Date: Thu, 1 Dec 2022 09:11:39 +0000
Subject: [PATCH 416/679] Translated using Weblate (Italian)

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/it/
---
 library/ui-strings/src/main/res/values-it/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml
index d244f26a43..d6a7858ebc 100644
--- a/library/ui-strings/src/main/res/values-it/strings.xml
+++ b/library/ui-strings/src/main/res/values-it/strings.xml
@@ -2805,7 +2805,7 @@
     L\'altro dispositivo ha già fatto l\'accesso.
     Si è verificato un problema di sicurezza configurando i messaggi sicuri. Una delle seguenti cose potrebbe essere compromessa: il tuo homeserver; la/e connessione/i internet; il/i dispositivo/i;
     La richiesta è fallita.
-    Buffering
+    Buffer…
     Sospendi trasmissione vocale
     Avvia o riprendi trasmissione vocale
     Ferma registrazione trasmissione vocale
@@ -2857,4 +2857,6 @@
     Citazione
     Risposta a %s
     Modifica
+    Mostra chat recenti nel menu di condivisione di sistema
+    Attiva condivisione diretta
 
\ No newline at end of file

From 3d84a999e06d015631613542d008ab26753d5eea Mon Sep 17 00:00:00 2001
From: Jozef Gaal 
Date: Wed, 30 Nov 2022 09:50:08 +0000
Subject: [PATCH 417/679] Translated using Weblate (Slovak)

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/
---
 library/ui-strings/src/main/res/values-sk/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml
index 078ffc44eb..f59073c5db 100644
--- a/library/ui-strings/src/main/res/values-sk/strings.xml
+++ b/library/ui-strings/src/main/res/values-sk/strings.xml
@@ -2868,7 +2868,7 @@
     Žiadosť zlyhala.
     Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti.
     Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja)
-    Načítavanie do vyrovnávacej pamäte
+    Načítavanie do vyrovnávacej pamäte…
     Pozastaviť hlasové vysielanie
     Prehrať alebo pokračovať v nahrávaní hlasového vysielania
     Zastaviť nahrávanie hlasového vysielania
@@ -2922,4 +2922,6 @@
     Zobraziť IP adresu
     Odpoveď na %s
     Úprava
+    Zobraziť posledné konverzácie v systémovej ponuke zdieľania
+    Povoliť priame zdieľanie
 
\ No newline at end of file

From b759f40c13eb1ba23a7e5ad5d5ebbaeb6dc18cce Mon Sep 17 00:00:00 2001
From: Besnik Bleta 
Date: Wed, 30 Nov 2022 16:23:19 +0000
Subject: [PATCH 418/679] Translated using Weblate (Albanian)

Currently translated at 99.3% (2541 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/
---
 library/ui-strings/src/main/res/values-sq/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml
index 800ec17dcf..58214e8a22 100644
--- a/library/ui-strings/src/main/res/values-sq/strings.xml
+++ b/library/ui-strings/src/main/res/values-sq/strings.xml
@@ -2659,7 +2659,7 @@
 \nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta.
     Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori
     Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm.
-    Përdo
+    
     Ndal transmetim zanor
     Luani ose vazhdoni luajtje transmetimi zanor
     Ndal incizim transmetimi zanor
@@ -2851,4 +2851,6 @@
     Kthim prapa 30 sekonda
     Si përgjigje për %s
     Aktivizo MD të lënë për më vonë
+    Tkurr pjella të %s
+    Zgjero pjella të %s
 
\ No newline at end of file

From 571d1a4816652f6a46b1b7eced2ef6625c4f5e1a Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk 
Date: Tue, 29 Nov 2022 22:10:18 +0000
Subject: [PATCH 419/679] Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/
---
 library/ui-strings/src/main/res/values-uk/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml
index d65a7a1da7..6a1c5355ab 100644
--- a/library/ui-strings/src/main/res/values-uk/strings.xml
+++ b/library/ui-strings/src/main/res/values-uk/strings.xml
@@ -2922,7 +2922,7 @@
     Запит не виконаний.
     Можливість записувати та надсилати голосові трансляції до стрічки кімнати.
     Увімкнути голосові трансляції (в активній розробці)
-    Буферизація
+    Буферизація…
     Призупинити голосову трансляцію
     Відтворити або поновити відтворення голосової трансляції
     Припинити запис голосової трансляції
@@ -2978,4 +2978,6 @@
     Цитуючи
     У відповідь %s
     Редагування
+    Показувати останні бесіди в системному меню загального доступу
+    Увімкнути пряме поширення
 
\ No newline at end of file

From a57162cf83aa03ec8959f1585a76621a27b04fde Mon Sep 17 00:00:00 2001
From: Jeff Huang 
Date: Wed, 30 Nov 2022 02:05:55 +0000
Subject: [PATCH 420/679] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2558 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/
---
 library/ui-strings/src/main/res/values-zh-rTW/strings.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
index dc5f6d85e3..9a5439b2ae 100644
--- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
+++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml
@@ -2760,7 +2760,7 @@
     請求失敗。
     可以在聊天室時間軸中錄製並傳送語音廣播。
     啟用語音廣播(正在積極開發中)
-    正在緩衝
+    正在緩衝……
     暫停語音廣播
     播放或繼續語音廣播
     停止語音廣播錄製
@@ -2810,4 +2810,6 @@
     引用
     回覆給 %s
     正在編輯
+    在系統分享選單中顯示最近聊天
+    啟用直接分享
 
\ No newline at end of file

From 56715f13d48d31bf6df88e07fb31c29d99d54ee3 Mon Sep 17 00:00:00 2001
From: Besnik Bleta 
Date: Wed, 30 Nov 2022 16:24:03 +0000
Subject: [PATCH 421/679] Translated using Weblate (Albanian)

Currently translated at 100.0% (82 of 82 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/
---
 fastlane/metadata/android/sq/changelogs/40105080.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/sq/changelogs/40105080.txt

diff --git a/fastlane/metadata/android/sq/changelogs/40105080.txt b/fastlane/metadata/android/sq/changelogs/40105080.txt
new file mode 100644
index 0000000000..b059e86cbd
--- /dev/null
+++ b/fastlane/metadata/android/sq/changelogs/40105080.txt
@@ -0,0 +1,2 @@
+Ndryshimet kryesore në këtë version: ndreqje të metash dhe përmirësime.
+Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases

From 0c11778d3354387de0f6762bb3590855318648b9 Mon Sep 17 00:00:00 2001
From: Jorge Martin Espinosa 
Date: Thu, 1 Dec 2022 11:26:55 +0100
Subject: [PATCH 422/679] Rich Text Editor: fix several inset issues in room
 screen (#7681)

---
 changelog.d/7680.bugfix                       |  3 +++
 .../utils/ExpandingBottomSheetBehavior.kt     | 23 ++++++++++++-------
 .../composer/MessageComposerFragment.kt       |  2 +-
 .../src/main/res/layout/fragment_timeline.xml |  2 +-
 4 files changed, 20 insertions(+), 10 deletions(-)
 create mode 100644 changelog.d/7680.bugfix

diff --git a/changelog.d/7680.bugfix b/changelog.d/7680.bugfix
new file mode 100644
index 0000000000..2e3b4b2e48
--- /dev/null
+++ b/changelog.d/7680.bugfix
@@ -0,0 +1,3 @@
+Rich Text Editor: fix several issues related to insets:
+* Empty space displayed at the bottom when you don't have permissions to send messages into a room.
+* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it.
diff --git a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt
index 0474cdea7e..47326bca76 100644
--- a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt
@@ -608,26 +608,33 @@ class ExpandingBottomSheetBehavior : CoordinatorLayout.Behavior {
         initialPaddingBottom = view.paddingBottom
 
         // This should only be used to set initial insets and other edge cases where the insets can't be applied using an animation.
-        var applyInsetsFromAnimation = false
+        var isAnimating = false
 
-        // This will animated inset changes, making them look a lot better. However, it won't update initial insets.
+        // This will animate inset changes, making them look a lot better. However, it won't update initial insets.
         ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
+            override fun onPrepare(animation: WindowInsetsAnimationCompat) {
+                isAnimating = true
+            }
+
             override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat {
-                return applyInsets(view, insets)
+                return if (isAnimating) {
+                    applyInsets(view, insets)
+                } else {
+                    insets
+                }
             }
 
             override fun onEnd(animation: WindowInsetsAnimationCompat) {
-                applyInsetsFromAnimation = false
+                isAnimating = false
                 view.requestApplyInsets()
             }
         })
 
         ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat ->
-            if (!applyInsetsFromAnimation) {
-                applyInsetsFromAnimation = true
-                applyInsets(view, insets)
-            } else {
+            if (isAnimating) {
                 insets
+            } else {
+                applyInsets(view, insets)
             }
         }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index d551850ff3..97e74785ec 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -255,7 +255,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
     ) { mainState, messageComposerState, attachmentState ->
         if (mainState.tombstoneEvent != null) return@withState
 
-        (composer as? View)?.isInvisible = !messageComposerState.isComposerVisible
+        (composer as? View)?.isVisible = messageComposerState.isComposerVisible
         composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
         (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
     }
diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml
index 6597b464ac..6e83dbe8fd 100644
--- a/vector/src/main/res/layout/fragment_timeline.xml
+++ b/vector/src/main/res/layout/fragment_timeline.xml
@@ -75,7 +75,7 @@
             android:layout_width="0dp"
             android:layout_height="0dp"
             android:overScrollMode="always"
-            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/notificationAreaView"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"

From da5db0ed15f68bfeb573ceac96380b26fcbc290e Mon Sep 17 00:00:00 2001
From: jonnyandrew 
Date: Thu, 1 Dec 2022 13:39:01 +0000
Subject: [PATCH 423/679] [Rich text editor] Fix keyboard closing after
 collapsing rich text editor (#7659)

---
 changelog.d/7659.bugfix                                        | 1 +
 .../home/room/detail/composer/RichTextComposerLayout.kt        | 3 ---
 2 files changed, 1 insertion(+), 3 deletions(-)
 create mode 100644 changelog.d/7659.bugfix

diff --git a/changelog.d/7659.bugfix b/changelog.d/7659.bugfix
new file mode 100644
index 0000000000..38be1008ef
--- /dev/null
+++ b/changelog.d/7659.bugfix
@@ -0,0 +1 @@
+[Rich text editor] Fix keyboard closing after collapsing editor
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index a058d4fdaa..48459b5c06 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -42,7 +42,6 @@ import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
 import com.google.android.material.shape.MaterialShapeDrawable
 import im.vector.app.R
-import im.vector.app.core.extensions.hideKeyboard
 import im.vector.app.core.extensions.setTextIfDifferent
 import im.vector.app.core.extensions.showKeyboard
 import im.vector.app.core.utils.DimensionConverter
@@ -132,8 +131,6 @@ class RichTextComposerLayout @JvmOverloads constructor(
         views.bottomSheetHandle.isVisible = isFullScreen
         if (isFullScreen) {
             editText.showKeyboard(true)
-        } else {
-            editText.hideKeyboard()
         }
         this.isFullScreen = isFullScreen
     }

From 341967bf3c31e0f247e6081f5ba0f6e5b32ec523 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 1 Dec 2022 15:25:54 +0100
Subject: [PATCH 424/679] Fix crash when invalid url is entered #7672

---
 .../onboarding/OnboardingViewModel.kt         | 21 ++++++++-----------
 1 file changed, 9 insertions(+), 12 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index 022fea5ed1..7fe73f8087 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor(
         }
     }
 
-    private fun checkQrCodeLoginCapability(homeServerUrl: String) {
+    private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) {
         if (!vectorFeatures.isQrCodeLoginEnabled()) {
             setState {
                 copy(
@@ -133,16 +133,12 @@ class OnboardingViewModel @AssistedInject constructor(
                 )
             }
         } else {
-            viewModelScope.launch {
-                // check if selected server supports MSC3882 first
-                homeServerConnectionConfigFactory.create(homeServerUrl)?.let {
-                    val canLoginWithQrCode = authenticationService.isQrLoginSupported(it)
-                    setState {
-                        copy(
-                                canLoginWithQrCode = canLoginWithQrCode
-                        )
-                    }
-                }
+            // check if selected server supports MSC3882 first
+            val canLoginWithQrCode = authenticationService.isQrLoginSupported(config)
+            setState {
+                copy(
+                        canLoginWithQrCode = canLoginWithQrCode
+                )
             }
         }
     }
@@ -710,7 +706,6 @@ class OnboardingViewModel @AssistedInject constructor(
             _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
         } else {
             startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
-            checkQrCodeLoginCapability(homeServerConnectionConfig.homeServerUri.toString())
         }
     }
 
@@ -769,6 +764,8 @@ class OnboardingViewModel @AssistedInject constructor(
             _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
         }
 
+        checkQrCodeLoginCapability(config)
+
         when (trigger) {
             is OnboardingAction.HomeServerChange.SelectHomeServer -> {
                 onHomeServerSelected(config, serverTypeOverride, authResult)

From d96ff6e5277b619a637bc2403889a285390bb191 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 1 Dec 2022 15:38:31 +0100
Subject: [PATCH 425/679] Changelog

---
 changelog.d/7684.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7684.bugfix

diff --git a/changelog.d/7684.bugfix b/changelog.d/7684.bugfix
new file mode 100644
index 0000000000..4a9af884a1
--- /dev/null
+++ b/changelog.d/7684.bugfix
@@ -0,0 +1 @@
+ Fix crash when invalid homeserver url is entered.

From d580d4cdb6b2a354ebade02450ce806acd792b4b Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 1 Dec 2022 16:25:40 +0100
Subject: [PATCH 426/679] Read sensible data from the env and do not rely to an
 external script anymore.

---
 tools/release/releaseScript.sh | 154 ++++++++++++++++++++++++++++-----
 1 file changed, 133 insertions(+), 21 deletions(-)

diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index 83572d4910..d76cd98061 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -19,43 +19,81 @@
 # Ignore any error to not stop the script
 set +e
 
-printf "\n"
-printf "================================================================================\n"
+printf "\n================================================================================\n"
 printf "|                    Welcome to the release script!                            |\n"
 printf "================================================================================\n"
 
-releaseScriptLocation="${RELEASE_SCRIPT_PATH}"
+printf "Checking environment...\n"
+envError=0
 
-if [[ -z "${releaseScriptLocation}" ]]; then
-    printf "Fatal: RELEASE_SCRIPT_PATH is not defined in the environment. Please set to the path of your local file 'releaseElement2.sh'.\n"
-    exit 1
+# Path of the key store (it's a file)
+keyStorePath="${ELEMENT_KEYSTORE_PATH}"
+if [[ -z "${keyStorePath}" ]]; then
+    printf "Fatal: ELEMENT_KEYSTORE_PATH is not defined in the environment.\n"
+    envError=1
+fi
+# Keystore password
+keyStorePassword="${ELEMENT_KEYSTORE_PASSWORD}"
+if [[ -z "${keyStorePassword}" ]]; then
+    printf "Fatal: ELEMENT_KEYSTORE_PASSWORD is not defined in the environment.\n"
+    envError=1
+fi
+# Key password
+keyPassword="${ELEMENT_KEY_PASSWORD}"
+if [[ -z "${keyPassword}" ]]; then
+    printf "Fatal: ELEMENT_KEY_PASSWORD is not defined in the environment.\n"
+    envError=1
+fi
+# GitHub token
+gitHubToken="${ELEMENT_GITHUB_TOKEN}"
+if [[ -z "${gitHubToken}" ]]; then
+    printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n"
+    envError=1
+fi
+# Android home
+androidHome="${ANDROID_HOME}"
+if [[ -z "${androidHome}" ]]; then
+    printf "Fatal: ANDROID_HOME is not defined in the environment.\n"
+    envError=1
+fi
+# @elementbot:matrix.org matrix token / Not mandatory
+elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}"
+if [[ -z "${elementBotToken}" ]]; then
+    printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n"
 fi
 
-releaseScriptFullPath="${releaseScriptLocation}/releaseElement2.sh"
-
-if [[ ! -f ${releaseScriptFullPath} ]]; then
-  printf "Fatal: release script not found at ${releaseScriptFullPath}.\n"
+if [ ${envError} == 1 ]; then
   exit 1
 fi
 
+buildToolsVersion="30.0.2"
+buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}"
+
+if [[ ! -d ${buildToolsPath} ]]; then
+    printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n"
+    exit 1
+fi
+
 # Check if git flow is enabled
 git flow config >/dev/null 2>&1
 if [[ $? == 0 ]]
 then
-    printf "Git flow is initialized"
+    printf "Git flow is initialized\n"
 else
     printf "Git flow is not initialized. Initializing...\n"
     # All default value, just set 'v' for tag prefix
     git flow init -d -t 'v'
 fi
 
+printf "OK\n"
+
+printf "\n================================================================================\n"
 # Guessing version to propose a default version
 versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut  -d " " -f3`
 versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut  -d " " -f3`
 versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut  -d " " -f3`
 versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}"
 
-printf "\n"
 read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version
 version=${version:-${versionCandidate}}
 
@@ -229,14 +267,89 @@ printf "Wait for the GitHub action https://github.com/vector-im/element-android/
 read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl
 
 printf "\n================================================================================\n"
-printf "Running the release script...\n"
-cd ${releaseScriptLocation}
-${releaseScriptFullPath} "v${version}" ${artifactUrl}
-cd -
+printf "Downloading the artifact...\n"
+
+# Download files
+targetPath="./tmp/Element/${version}"
+
+# Ignore error
+set +e
+
+python3 ./tools/release/download_github_artifacts.py \
+    --token ${gitHubToken} \
+    --artifactUrl ${artifactUrl} \
+    --directory ${targetPath} \
+    --ignoreErrors
+
+# Do not ignore error
+set -e
+
+printf "\n================================================================================\n"
+printf "Unzipping the artifact...\n"
+
+unzip ${targetPath}/vector-gplay-release-unsigned.zip -d ${targetPath}
+
+# Flatten folder hierarchy
+mv ${targetPath}/gplay/release/* ${targetPath}
+rm -rf ${targetPath}/gplay
+
+printf "\n================================================================================\n"
+printf "Signing the APKs...\n"
+
+cp ${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk \
+   ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk
+./tools/release/sign_apk_unsafe.sh \
+    ${keyStorePath} \
+    ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk \
+    ${keyStorePassword} \
+    ${keyPassword}
+
+cp ${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk \
+   ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk
+./tools/release/sign_apk_unsafe.sh \
+    ${keyStorePath} \
+    ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk \
+    ${keyStorePassword} \
+    ${keyPassword}
+
+cp ${targetPath}/vector-gplay-x86-release-unsigned.apk \
+   ${targetPath}/vector-gplay-x86-release-signed.apk
+./tools/release/sign_apk_unsafe.sh \
+    ${keyStorePath} \
+    ${targetPath}/vector-gplay-x86-release-signed.apk \
+    ${keyStorePassword} \
+    ${keyPassword}
+
+cp ${targetPath}/vector-gplay-x86_64-release-unsigned.apk \
+   ${targetPath}/vector-gplay-x86_64-release-signed.apk
+./tools/release/sign_apk_unsafe.sh \
+    ${keyStorePath} \
+    ${targetPath}/vector-gplay-x86_64-release-signed.apk \
+    ${keyStorePassword} \
+    ${keyPassword}
+
+# Ref: https://docs.fastlane.tools/getting-started/android/beta-deployment/#uploading-your-app
+# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-x86-release-unsigned.apk,${targetPath}/vector-gplay-x86_64-release-unsigned.apk"
+#
+# ./fastlane beta
+
+printf "\n================================================================================\n"
+printf "Please check the information below:\n"
+
+printf "File vector-gplay-arm64-v8a-release-signed.apk:\n"
+${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk | grep package
+printf "File vector-gplay-armeabi-v7a-release-signed.apk:\n"
+${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk | grep package
+printf "File vector-gplay-x86-release-signed.apk:\n"
+${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signed.apk | grep package
+printf "File vector-gplay-x86_64-release-signed.apk:\n"
+${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package
+
+read -p "\nDoes it look correct? Press enter when it's done."
 
 printf "\n================================================================================\n"
 read -p "Installing apk on a real device, press enter when a real device is connected. "
-apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk"
+apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk"
 adb -d install ${apkPath}
 
 read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done."
@@ -250,9 +363,8 @@ printf "Message for the Android internal room:\n\n"
 message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
 printf "${message}\n\n"
 
-matrixOrgToken="${MATRIX_ORG_TOKEN}"
-if [[ -z "${matrixOrgToken}" ]]; then
-  read -p "MATRIX_ORG_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done "
+if [[ -z "${elementBotToken}" ]]; then
+  read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done "
 else
   read -p "Send this message to the room (yes/no) default to yes? " doSend
   doSend=${doSend:-yes}
@@ -261,7 +373,7 @@ else
     transactionId=`openssl rand -hex 16`
     # Element Android internal
     matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org"
-    curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${matrixOrgToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId}
+    curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId}
   else
     printf "Message not sent, please send it manually!\n"
   fi

From 381103383ec983b3232770214946fb3ce0e95671 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 1 Dec 2022 17:44:12 +0100
Subject: [PATCH 427/679] Fix unit tests.

---
 .../vector/app/features/onboarding/OnboardingViewModelTest.kt | 3 +++
 .../im/vector/app/test/fakes/FakeAuthenticationService.kt     | 4 ++++
 2 files changed, 7 insertions(+)

diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index 718f1ec7a9..1666491afb 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -1152,11 +1152,13 @@ class OnboardingViewModelTest {
             resultingState: SelectedHomeserverState,
             config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
             fingerprint: Fingerprint? = null,
+            canLoginWithQrCode: Boolean = false,
     ) {
         fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
         fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
         givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
         fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
+        fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode)
     }
 
     private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
@@ -1164,6 +1166,7 @@ class OnboardingViewModelTest {
         fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
         givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
         fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
+        fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false)
     }
 
     private fun givenUserNameIsAvailable(userName: String) {
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
index af53913169..5d0e317c57 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
@@ -58,6 +58,10 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
         coEvery { getWellKnownData(matrixId, config) } returns result
     }
 
+    fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) {
+        coEvery { isQrLoginSupported(config) } returns result
+    }
+
     fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) {
         coEvery { getWellKnownData(matrixId, config) } throws cause
     }

From b6aae0c7c10162ba1f502bc4f84b0cc8de196ebb Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 1 Dec 2022 17:51:44 +0100
Subject: [PATCH 428/679] Add unit test for canLoginWithQrCode = true

---
 .../onboarding/OnboardingViewModelTest.kt     | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index 1666491afb..92083eb50b 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -160,6 +160,28 @@ class OnboardingViewModelTest {
                 .finish()
     }
 
+    @Test
+    fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
+        val test = viewModel.test()
+        fakeVectorFeatures.givenCombinedLoginEnabled()
+        givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true)
+
+        viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
+
+        test
+                .assertStatesChanges(
+                        initialState,
+                        { copy(onboardingFlow = OnboardingFlow.SignIn) },
+                        { copy(isLoading = true) },
+                        { copy(canLoginWithQrCode = true) },
+                        { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) },
+                        { copy(signMode = SignMode.SignIn) },
+                        { copy(isLoading = false) }
+                )
+                .assertEvents(OnboardingViewEvents.OpenCombinedLogin)
+                .finish()
+    }
+
     @Test
     fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest {
         val test = viewModel.test()

From c12906971a2ee19218127295f78d364f8750f380 Mon Sep 17 00:00:00 2001
From: Jonny Andrew 
Date: Thu, 1 Dec 2022 17:15:23 +0000
Subject: [PATCH 429/679] Move changelog entry to correct dir

---
 7658.bugfix => changelog.d/7658.bugfix | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename 7658.bugfix => changelog.d/7658.bugfix (100%)

diff --git a/7658.bugfix b/changelog.d/7658.bugfix
similarity index 100%
rename from 7658.bugfix
rename to changelog.d/7658.bugfix

From b70370b21704e5908a9c18438be08cd8eb337930 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 1 Dec 2022 23:02:57 +0000
Subject: [PATCH 430/679] Bump soloader from 0.10.4 to 0.10.5

Bumps [soloader](https://github.com/facebook/soloader) from 0.10.4 to 0.10.5.
- [Release notes](https://github.com/facebook/soloader/releases)
- [Commits](https://github.com/facebook/soloader/commits)

---
updated-dependencies:
- dependency-name: com.facebook.soloader:soloader
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 vector-app/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index 68e20996ad..1096c7ee63 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -359,7 +359,7 @@ dependencies {
     debugImplementation(libs.flipper.flipperNetworkPlugin) {
         exclude group: 'com.facebook.fbjni', module: 'fbjni'
     }
-    debugImplementation 'com.facebook.soloader:soloader:0.10.4'
+    debugImplementation 'com.facebook.soloader:soloader:0.10.5'
     debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0"
 
     gplayImplementation "com.google.android.gms:play-services-location:21.0.1"

From f0ad75a2b7f0ac152306952c6e466b14a75dc47e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 1 Dec 2022 23:03:43 +0000
Subject: [PATCH 431/679] Bump flipper from 0.175.0 to 0.176.0

Bumps `flipper` from 0.175.0 to 0.176.0.

Updates `flipper` from 0.175.0 to 0.176.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.175.0...v0.176.0)

Updates `flipper-network-plugin` from 0.175.0 to 0.176.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.175.0...v0.176.0)

---
updated-dependencies:
- dependency-name: com.facebook.flipper:flipper
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.facebook.flipper:flipper-network-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index cb10170e93..27bca6434f 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -17,7 +17,7 @@ def markwon = "4.6.2"
 def moshi = "1.14.0"
 def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
-def flipper = "0.175.0"
+def flipper = "0.176.0"
 def epoxy = "5.0.0"
 def mavericks = "3.0.1"
 def glide = "4.14.2"

From 20b1eaba9e89bda737a61faec5cb3340ff5a1282 Mon Sep 17 00:00:00 2001
From: jonnyandrew 
Date: Fri, 2 Dec 2022 08:41:33 +0000
Subject: [PATCH 432/679] Fix crash in message composer when room is missing
 (#7683)

This error was seen before but has been reintroduced during refactoring.
- see https://github.com/vector-im/element-android/pull/6978
---
 changelog.d/7683.bugfix                       |   2 +
 .../composer/MessageComposerViewModel.kt      | 234 +++++++++---------
 .../composer/MessageComposerViewState.kt      |   5 +-
 3 files changed, 127 insertions(+), 114 deletions(-)
 create mode 100644 changelog.d/7683.bugfix

diff --git a/changelog.d/7683.bugfix b/changelog.d/7683.bugfix
new file mode 100644
index 0000000000..3922253ba6
--- /dev/null
+++ b/changelog.d/7683.bugfix
@@ -0,0 +1,2 @@
+Fix crash in message composer when room is missing
+
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index a8be2be5e2..c02eb1fa8a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.getRoomSummary
+import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.api.session.room.getStateEvent
 import org.matrix.android.sdk.api.session.room.getTimelineEvent
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
@@ -89,39 +90,44 @@ class MessageComposerViewModel @AssistedInject constructor(
         private val voiceBroadcastHelper: VoiceBroadcastHelper,
 ) : VectorViewModel(initialState) {
 
-    private val room = session.getRoom(initialState.roomId)!!
+    private val room = session.getRoom(initialState.roomId)
 
     // Keep it out of state to avoid invalidate being called
     private var currentComposerText: CharSequence = ""
 
     init {
-        loadDraftIfAny()
-        observePowerLevelAndEncryption()
-        observeVoiceBroadcast()
-        subscribeToStateInternal()
+        if (room != null) {
+            loadDraftIfAny(room)
+            observePowerLevelAndEncryption(room)
+            observeVoiceBroadcast(room)
+            subscribeToStateInternal()
+        } else {
+            onRoomError()
+        }
     }
 
     override fun handle(action: MessageComposerAction) {
+        val room = this.room ?: return
         when (action) {
-            is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
-            is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
+            is MessageComposerAction.EnterEditMode -> handleEnterEditMode(room, action)
+            is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(room, action)
             is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
-            is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
-            is MessageComposerAction.SendMessage -> handleSendMessage(action)
-            is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
+            is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(room, action)
+            is MessageComposerAction.SendMessage -> handleSendMessage(room, action)
+            is MessageComposerAction.UserIsTyping -> handleUserIsTyping(room, action)
             is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
             is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
-            is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
-            is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId)
+            is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage(room)
+            is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(room, action.isCancelled, action.rootThreadEventId)
             is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
             MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
             MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
-            is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
-            is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
+            is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(room, action.attachmentData)
+            is MessageComposerAction.OnEntersBackground -> handleEntersBackground(room, action.composerText)
             is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
             is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
             is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
-            is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
+            is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(room, action)
             is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
             is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
         }
@@ -157,7 +163,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         copy(sendMode = SendMode.Regular(currentComposerText, action.fromSharing))
     }
 
-    private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
+    private fun handleEnterEditMode(room: Room, action: MessageComposerAction.EnterEditMode) {
         room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
             val formatted = vectorPreferences.isRichTextEditorEnabled()
             setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent(formatted))) }
@@ -168,7 +174,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         setState { copy(isFullScreen = action.isFullScreen) }
     }
 
-    private fun observePowerLevelAndEncryption() {
+    private fun observePowerLevelAndEncryption(room: Room) {
         combine(
                 PowerLevelsFlowFactory(room).createFlow(),
                 room.flow().liveRoomSummary().unwrap()
@@ -194,7 +200,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun observeVoiceBroadcast() {
+    private fun observeVoiceBroadcast(room: Room) {
         room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId))
                 .asFlow()
                 .unwrap()
@@ -204,19 +210,19 @@ class MessageComposerViewModel @AssistedInject constructor(
                 }
     }
 
-    private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
+    private fun handleEnterQuoteMode(room: Room, action: MessageComposerAction.EnterQuoteMode) {
         room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
             setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) }
         }
     }
 
-    private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
+    private fun handleEnterReplyMode(room: Room, action: MessageComposerAction.EnterReplyMode) {
         room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
             setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) }
         }
     }
 
-    private fun handleSendMessage(action: MessageComposerAction.SendMessage) {
+    private fun handleSendMessage(room: Room, action: MessageComposerAction.SendMessage) {
         withState { state ->
             analyticsTracker.capture(state.toAnalyticsComposer()).also {
                 setState { copy(startsThread = false) }
@@ -246,7 +252,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             }
 
                             _viewEvents.post(MessageComposerViewEvents.MessageSent)
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.ErrorSyntax -> {
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command))
@@ -272,7 +278,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false)
                             }
                             _viewEvents.post(MessageComposerViewEvents.MessageSent)
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendFormattedText -> {
                             // Send the text message to the room, without markdown
@@ -290,23 +296,23 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 )
                             }
                             _viewEvents.post(MessageComposerViewEvents.MessageSent)
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.ChangeRoomName -> {
-                            handleChangeRoomNameSlashCommand(parsedCommand)
+                            handleChangeRoomNameSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.Invite -> {
-                            handleInviteSlashCommand(parsedCommand)
+                            handleInviteSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.Invite3Pid -> {
-                            handleInvite3pidSlashCommand(parsedCommand)
+                            handleInvite3pidSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.SetUserPowerLevel -> {
-                            handleSetUserPowerLevel(parsedCommand)
+                            handleSetUserPowerLevel(room, parsedCommand)
                         }
                         is ParsedCommand.DevTools -> {
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.ClearScalarToken -> {
                             // TODO
@@ -315,29 +321,29 @@ class MessageComposerViewModel @AssistedInject constructor(
                         is ParsedCommand.SetMarkdown -> {
                             vectorPreferences.setMarkdownEnabled(parsedCommand.enable)
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.BanUser -> {
-                            handleBanSlashCommand(parsedCommand)
+                            handleBanSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.UnbanUser -> {
-                            handleUnbanSlashCommand(parsedCommand)
+                            handleUnbanSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.IgnoreUser -> {
-                            handleIgnoreSlashCommand(parsedCommand)
+                            handleIgnoreSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.UnignoreUser -> {
                             handleUnignoreSlashCommand(parsedCommand)
                         }
                         is ParsedCommand.RemoveUser -> {
-                            handleRemoveSlashCommand(parsedCommand)
+                            handleRemoveSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.JoinRoom -> {
                             handleJoinToAnotherRoomSlashCommand(parsedCommand)
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.PartRoom -> {
-                            handlePartSlashCommand(parsedCommand)
+                            handlePartSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.SendEmote -> {
                             if (state.rootThreadEventId != null) {
@@ -355,7 +361,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 )
                             }
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendRainbow -> {
                             val message = parsedCommand.message.toString()
@@ -369,7 +375,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message))
                             }
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendRainbowEmote -> {
                             val message = parsedCommand.message.toString()
@@ -385,7 +391,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             }
 
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendSpoiler -> {
                             val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})"
@@ -403,53 +409,53 @@ class MessageComposerViewModel @AssistedInject constructor(
                                 )
                             }
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendShrug -> {
-                            sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId)
+                            sendPrefixedMessage(room, "¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId)
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendLenny -> {
-                            sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId)
+                            sendPrefixedMessage(room, "( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId)
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendTableFlip -> {
-                            sendPrefixedMessage("(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId)
+                            sendPrefixedMessage(room, "(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId)
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.SendChatEffect -> {
-                            sendChatEffect(parsedCommand)
+                            sendChatEffect(room, parsedCommand)
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.ChangeTopic -> {
-                            handleChangeTopicSlashCommand(parsedCommand)
+                            handleChangeTopicSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.ChangeDisplayName -> {
-                            handleChangeDisplayNameSlashCommand(parsedCommand)
+                            handleChangeDisplayNameSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.ChangeDisplayNameForRoom -> {
-                            handleChangeDisplayNameForRoomSlashCommand(parsedCommand)
+                            handleChangeDisplayNameForRoomSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.ChangeRoomAvatar -> {
-                            handleChangeRoomAvatarSlashCommand(parsedCommand)
+                            handleChangeRoomAvatarSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.ChangeAvatarForRoom -> {
-                            handleChangeAvatarForRoomSlashCommand(parsedCommand)
+                            handleChangeAvatarForRoomSlashCommand(room, parsedCommand)
                         }
                         is ParsedCommand.ShowUser -> {
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
                             handleWhoisSlashCommand(parsedCommand)
-                            popDraft()
+                            popDraft(room)
                         }
                         is ParsedCommand.DiscardSession -> {
                             if (room.roomCryptoService().isEncrypted()) {
                                 session.cryptoService().discardOutboundSession(room.roomId)
                                 _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                                popDraft()
+                                popDraft(room)
                             } else {
                                 _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
                                 _viewEvents.post(
@@ -474,7 +480,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                                     null,
                                                     true
                                             )
-                                    popDraft()
+                                    popDraft(room)
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
                                 } catch (failure: Throwable) {
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@@ -493,7 +499,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                                     null,
                                                     false
                                             )
-                                    popDraft()
+                                    popDraft(room)
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
                                 } catch (failure: Throwable) {
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@@ -506,7 +512,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             viewModelScope.launch(Dispatchers.IO) {
                                 try {
                                     session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias)
-                                    popDraft()
+                                    popDraft(room)
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
                                 } catch (failure: Throwable) {
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@@ -518,7 +524,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             viewModelScope.launch(Dispatchers.IO) {
                                 try {
                                     session.roomService().leaveRoom(parsedCommand.roomId)
-                                    popDraft()
+                                    popDraft(room)
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
                                 } catch (failure: Throwable) {
                                     _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@@ -534,7 +540,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                                     )
                             )
                             _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
-                            popDraft()
+                            popDraft(room)
                         }
                     }
                 }
@@ -583,7 +589,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                         }
                     }
                     _viewEvents.post(MessageComposerViewEvents.MessageSent)
-                    popDraft()
+                    popDraft(room)
                 }
                 is SendMode.Quote -> {
                     room.sendService().sendQuotedTextMessage(
@@ -594,7 +600,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                             rootThreadEventId = state.rootThreadEventId
                     )
                     _viewEvents.post(MessageComposerViewEvents.MessageSent)
-                    popDraft()
+                    popDraft(room)
                 }
                 is SendMode.Reply -> {
                     val timelineEvent = state.sendMode.timelineEvent
@@ -619,7 +625,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                     )
 
                     _viewEvents.post(MessageComposerViewEvents.MessageSent)
-                    popDraft()
+                    popDraft(room)
                 }
                 is SendMode.Voice -> {
                     // do nothing
@@ -628,10 +634,10 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun popDraft() = withState {
+    private fun popDraft(room: Room) = withState {
         if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) {
             // If we were sharing, we want to get back our last value from draft
-            loadDraftIfAny()
+            loadDraftIfAny(room)
         } else {
             // Otherwise we clear the composer and remove the draft from db
             setState { copy(sendMode = SendMode.Regular("", false)) }
@@ -641,7 +647,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun loadDraftIfAny() {
+    private fun loadDraftIfAny(room: Room) {
         val currentDraft = room.draftService().getDraft()
         setState {
             copy(
@@ -670,7 +676,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) {
+    private fun handleUserIsTyping(room: Room, action: MessageComposerAction.UserIsTyping) {
         if (vectorPreferences.sendTypingNotifs()) {
             if (action.isTyping) {
                 room.typingService().userIsTyping()
@@ -680,7 +686,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) {
+    private fun sendChatEffect(room: Room, sendChatEffect: ParsedCommand.SendChatEffect) {
         // If message is blank, convert to an emote, with default message
         if (sendChatEffect.message.isBlank()) {
             val defaultMessage = stringProvider.getString(
@@ -732,25 +738,25 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
-        launchSlashCommandFlowSuspendable(changeTopic) {
+    private fun handleChangeTopicSlashCommand(room: Room, changeTopic: ParsedCommand.ChangeTopic) {
+        launchSlashCommandFlowSuspendable(room, changeTopic) {
             room.stateService().updateTopic(changeTopic.topic)
         }
     }
 
-    private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
-        launchSlashCommandFlowSuspendable(invite) {
+    private fun handleInviteSlashCommand(room: Room, invite: ParsedCommand.Invite) {
+        launchSlashCommandFlowSuspendable(room, invite) {
             room.membershipService().invite(invite.userId, invite.reason)
         }
     }
 
-    private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
-        launchSlashCommandFlowSuspendable(invite) {
+    private fun handleInvite3pidSlashCommand(room: Room, invite: ParsedCommand.Invite3Pid) {
+        launchSlashCommandFlowSuspendable(room, invite) {
             room.membershipService().invite3pid(invite.threePid)
         }
     }
 
-    private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
+    private fun handleSetUserPowerLevel(room: Room, setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
         val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
                 ?.content
                 ?.toModel()
@@ -758,19 +764,19 @@ class MessageComposerViewModel @AssistedInject constructor(
                 ?.toContent()
                 ?: return
 
-        launchSlashCommandFlowSuspendable(setUserPowerLevel) {
+        launchSlashCommandFlowSuspendable(room, setUserPowerLevel) {
             room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent)
         }
     }
 
-    private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) {
-        launchSlashCommandFlowSuspendable(changeDisplayName) {
+    private fun handleChangeDisplayNameSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayName) {
+        launchSlashCommandFlowSuspendable(room, changeDisplayName) {
             session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName)
         }
     }
 
-    private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) {
-        launchSlashCommandFlowSuspendable(command) {
+    private fun handlePartSlashCommand(room: Room, command: ParsedCommand.PartRoom) {
+        launchSlashCommandFlowSuspendable(room, command) {
             if (command.roomAlias == null) {
                 // Leave the current room
                 room
@@ -785,39 +791,39 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) {
-        launchSlashCommandFlowSuspendable(removeUser) {
+    private fun handleRemoveSlashCommand(room: Room, removeUser: ParsedCommand.RemoveUser) {
+        launchSlashCommandFlowSuspendable(room, removeUser) {
             room.membershipService().remove(removeUser.userId, removeUser.reason)
         }
     }
 
-    private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) {
-        launchSlashCommandFlowSuspendable(ban) {
+    private fun handleBanSlashCommand(room: Room, ban: ParsedCommand.BanUser) {
+        launchSlashCommandFlowSuspendable(room, ban) {
             room.membershipService().ban(ban.userId, ban.reason)
         }
     }
 
-    private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) {
-        launchSlashCommandFlowSuspendable(unban) {
+    private fun handleUnbanSlashCommand(room: Room, unban: ParsedCommand.UnbanUser) {
+        launchSlashCommandFlowSuspendable(room, unban) {
             room.membershipService().unban(unban.userId, unban.reason)
         }
     }
 
-    private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) {
-        launchSlashCommandFlowSuspendable(changeRoomName) {
+    private fun handleChangeRoomNameSlashCommand(room: Room, changeRoomName: ParsedCommand.ChangeRoomName) {
+        launchSlashCommandFlowSuspendable(room, changeRoomName) {
             room.stateService().updateName(changeRoomName.name)
         }
     }
 
-    private fun getMyRoomMemberContent(): RoomMemberContent? {
+    private fun getMyRoomMemberContent(room: Room): RoomMemberContent? {
         return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId))
                 ?.content
                 ?.toModel()
     }
 
-    private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
-        launchSlashCommandFlowSuspendable(changeDisplayName) {
-            getMyRoomMemberContent()
+    private fun handleChangeDisplayNameForRoomSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
+        launchSlashCommandFlowSuspendable(room, changeDisplayName) {
+            getMyRoomMemberContent(room)
                     ?.copy(displayName = changeDisplayName.displayName)
                     ?.toContent()
                     ?.let {
@@ -826,15 +832,15 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) {
-        launchSlashCommandFlowSuspendable(changeAvatar) {
+    private fun handleChangeRoomAvatarSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeRoomAvatar) {
+        launchSlashCommandFlowSuspendable(room, changeAvatar) {
             room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent())
         }
     }
 
-    private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) {
-        launchSlashCommandFlowSuspendable(changeAvatar) {
-            getMyRoomMemberContent()
+    private fun handleChangeAvatarForRoomSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeAvatarForRoom) {
+        launchSlashCommandFlowSuspendable(room, changeAvatar) {
+            getMyRoomMemberContent(room)
                     ?.copy(avatarUrl = changeAvatar.url)
                     ?.toContent()
                     ?.let {
@@ -843,8 +849,8 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) {
-        launchSlashCommandFlowSuspendable(ignore) {
+    private fun handleIgnoreSlashCommand(room: Room, ignore: ParsedCommand.IgnoreUser) {
+        launchSlashCommandFlowSuspendable(room, ignore) {
             session.userService().ignoreUserIds(listOf(ignore.userId))
         }
     }
@@ -853,15 +859,15 @@ class MessageComposerViewModel @AssistedInject constructor(
         _viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore))
     }
 
-    private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) {
+    private fun handleSlashCommandConfirmed(room: Room, action: MessageComposerAction.SlashCommandConfirmed) {
         when (action.parsedCommand) {
-            is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand)
+            is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(room, action.parsedCommand)
             else -> TODO("Not handled yet")
         }
     }
 
-    private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) {
-        launchSlashCommandFlowSuspendable(unignore) {
+    private fun handleUnignoreSlashCommandConfirmed(room: Room, unignore: ParsedCommand.UnignoreUser) {
+        launchSlashCommandFlowSuspendable(room, unignore) {
             session.userService().unIgnoreUserIds(listOf(unignore.userId))
         }
     }
@@ -870,7 +876,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         _viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId))
     }
 
-    private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) {
+    private fun sendPrefixedMessage(room: Room, prefix: String, message: CharSequence, rootThreadEventId: String?) {
         val sequence = buildString {
             append(prefix)
             if (message.isNotEmpty()) {
@@ -886,7 +892,7 @@ class MessageComposerViewModel @AssistedInject constructor(
     /**
      * Convert a send mode to a draft and save the draft.
      */
-    private fun handleSaveTextDraft(draft: String) = withState {
+    private fun handleSaveTextDraft(room: Room, draft: String) = withState {
         session.coroutineScope.launch {
             when {
                 it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
@@ -909,7 +915,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleStartRecordingVoiceMessage() {
+    private fun handleStartRecordingVoiceMessage(room: Room) {
         try {
             audioMessageHelper.startRecording(room.roomId)
         } catch (failure: Throwable) {
@@ -917,7 +923,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
+    private fun handleEndRecordingVoiceMessage(room: Room, isCancelled: Boolean, rootThreadEventId: String? = null) {
         audioMessageHelper.stopPlayback()
         if (isCancelled) {
             audioMessageHelper.deleteRecording()
@@ -964,7 +970,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         audioMessageHelper.stopAllVoiceActions(deleteRecord)
     }
 
-    private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
+    private fun handleInitializeVoiceRecorder(room: Room, attachmentData: ContentAttachmentData) {
         audioMessageHelper.initializeRecorder(room.roomId, attachmentData)
         setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
     }
@@ -985,7 +991,7 @@ class MessageComposerViewModel @AssistedInject constructor(
         audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
     }
 
-    private fun handleEntersBackground(composerText: String) {
+    private fun handleEntersBackground(room: Room, composerText: String) {
         // Always stop all voice actions. It may be playing in timeline or active recording
         val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
         // TODO remove this when there will be a listening indicator outside of the timeline
@@ -1001,7 +1007,7 @@ class MessageComposerViewModel @AssistedInject constructor(
                 }
             }
         } else {
-            handleSaveTextDraft(draft = composerText)
+            handleSaveTextDraft(room = room, draft = composerText)
         }
     }
 
@@ -1009,12 +1015,12 @@ class MessageComposerViewModel @AssistedInject constructor(
         _viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId))
     }
 
-    private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) {
+    private fun launchSlashCommandFlowSuspendable(room: Room, parsedCommand: ParsedCommand, block: suspend () -> Unit) {
         _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
         viewModelScope.launch {
             val event = try {
                 block()
-                popDraft()
+                popDraft(room)
                 MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)
             } catch (failure: Throwable) {
                 MessageComposerViewEvents.SlashCommandResultError(failure)
@@ -1023,6 +1029,10 @@ class MessageComposerViewModel @AssistedInject constructor(
         }
     }
 
+    private fun onRoomError() = setState {
+        copy(isRoomError = true)
+    }
+
     @AssistedFactory
     interface Factory : MavericksAssistedViewModelFactory {
         override fun create(initialState: MessageComposerViewState): MessageComposerViewModel
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
index bf40c18995..235ca574fc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt
@@ -62,6 +62,7 @@ fun CanSendStatus.boolean(): Boolean {
 
 data class MessageComposerViewState(
         val roomId: String,
+        val isRoomError: Boolean = false,
         val canSendMessage: CanSendStatus = CanSendStatus.Allowed,
         val isSendButtonVisible: Boolean = false,
         val rootThreadEventId: String? = null,
@@ -88,8 +89,8 @@ data class MessageComposerViewState(
 
     val isVoiceMessageIdle = !isVoiceRecording
 
-    val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording
-    val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible
+    val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording && !isRoomError
+    val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible && !isRoomError
 
     constructor(args: TimelineArgs) : this(
             roomId = args.roomId,

From 310ea99c443ef01083241bfc542ee34c7b1674a0 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Fri, 2 Dec 2022 10:50:08 +0100
Subject: [PATCH 433/679] Fix bad pills color background. For light and dark
 theme the color is now 61708B (iso EleWeb)

---
 changelog.d/7274.bugfix                               | 1 +
 library/ui-styles/src/main/res/values/palette.xml     | 2 +-
 library/ui-styles/src/main/res/values/theme_dark.xml  | 2 +-
 library/ui-styles/src/main/res/values/theme_light.xml | 2 +-
 4 files changed, 4 insertions(+), 3 deletions(-)
 create mode 100644 changelog.d/7274.bugfix

diff --git a/changelog.d/7274.bugfix b/changelog.d/7274.bugfix
new file mode 100644
index 0000000000..e99daceb89
--- /dev/null
+++ b/changelog.d/7274.bugfix
@@ -0,0 +1 @@
+Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb)
diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml
index 73ac768919..999dccf167 100644
--- a/library/ui-styles/src/main/res/values/palette.xml
+++ b/library/ui-styles/src/main/res/values/palette.xml
@@ -44,4 +44,4 @@
     #15191E
     #21262C
 
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index d5aaa88ab8..9665b7335c 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -53,7 +53,7 @@
         ?vctr_content_quinary
         ?vctr_system
         ?vctr_system
-        ?vctr_content_tertiary
+        ?vctr_notice_secondary
 
         
         @color/element_accent_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index 1978db9139..c19fe8a111 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -53,7 +53,7 @@
         ?vctr_content_quinary
         ?vctr_system
         ?vctr_system
-        ?vctr_content_tertiary
+        ?vctr_notice_secondary
 
         
         @color/element_accent_light

From 4050975a19045d2c1152f7ee5e4dde6d27c3431f Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 2 Dec 2022 18:15:10 +0300
Subject: [PATCH 434/679] Implement new logic for new login banner.

---
 .../debug/features/DebugVectorFeatures.kt     |  4 ++
 .../im/vector/app/features/VectorFeatures.kt  |  2 +-
 .../app/features/home/HomeDetailFragment.kt   |  4 +-
 .../home/IsNewLoginAlertShownUseCase.kt       | 31 ++++++++++++++
 .../features/home/NewHomeDetailFragment.kt    |  4 +-
 .../home/SetNewLoginAlertShownUseCase.kt      | 31 ++++++++++++++
 .../SetUnverifiedSessionsAlertShownUseCase.kt | 34 +++++++++++++++
 ...houldShowUnverifiedSessionsAlertUseCase.kt |  6 ++-
 .../UnknownDeviceDetectorSharedViewModel.kt   | 41 ++++++-------------
 .../features/settings/VectorPreferences.kt    | 35 ++++++++--------
 ...dShowUnverifiedSessionsAlertUseCaseTest.kt | 11 ++---
 .../app/test/fakes/FakeVectorPreferences.kt   |  2 +-
 12 files changed, 145 insertions(+), 60 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt

diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
index 5c497c24ec..2134c8cf2c 100644
--- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
+++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
@@ -88,6 +88,9 @@ class DebugVectorFeatures(
     override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled)
             ?: vectorFeatures.isVoiceBroadcastEnabled()
 
+    override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled)
+            ?: vectorFeatures.isUnverifiedSessionsAlertEnabled()
+
     fun  override(value: T?, key: Preferences.Key) = updatePreferences {
         if (value == null) {
             it.remove(key)
@@ -151,4 +154,5 @@ object DebugFeatureKeys {
     val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers")
     val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login")
     val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled")
+    val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled")
 }
diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
index 28c2e37926..99abc15f81 100644
--- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
+++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt
@@ -64,5 +64,5 @@ class DefaultVectorFeatures : VectorFeatures {
     override fun isQrCodeLoginForAllServers(): Boolean = false
     override fun isReciprocateQrCodeLogin(): Boolean = false
     override fun isVoiceBroadcastEnabled(): Boolean = true
-    override fun isUnverifiedSessionsAlertEnabled(): Boolean = false
+    override fun isUnverifiedSessionsAlertEnabled(): Boolean = true
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
index 7552b934e4..69abeed424 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
@@ -239,12 +239,12 @@ class HomeDetailFragment :
                                     .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "")
                         }
                         unknownDeviceDetectorSharedViewModel.handle(
-                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
+                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
                         )
                     }
                     dismissedAction = Runnable {
                         unknownDeviceDetectorSharedViewModel.handle(
-                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
+                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
                         )
                     }
                 }
diff --git a/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt
new file mode 100644
index 0000000000..5a0d4743dc
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home
+
+import im.vector.app.features.settings.VectorPreferences
+import javax.inject.Inject
+
+class IsNewLoginAlertShownUseCase @Inject constructor(
+        private val vectorPreferences: VectorPreferences,
+) {
+
+    fun execute(deviceId: String?): Boolean {
+        deviceId ?: return false
+
+        return vectorPreferences.isNewLoginAlertShownForDevice(deviceId)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
index 62d7e58bdb..ccd5a7e84b 100644
--- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
@@ -253,12 +253,12 @@ class NewHomeDetailFragment :
                                     .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "")
                         }
                         unknownDeviceDetectorSharedViewModel.handle(
-                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
+                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
                         )
                     }
                     dismissedAction = Runnable {
                         unknownDeviceDetectorSharedViewModel.handle(
-                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
+                                UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty())
                         )
                     }
                 }
diff --git a/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt
new file mode 100644
index 0000000000..d313f93043
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home
+
+import im.vector.app.features.settings.VectorPreferences
+import javax.inject.Inject
+
+class SetNewLoginAlertShownUseCase @Inject constructor(
+        private val vectorPreferences: VectorPreferences,
+) {
+
+    fun execute(deviceIds: List) {
+        deviceIds.forEach {
+            vectorPreferences.setNewLoginAlertShownForDevice(it)
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt
new file mode 100644
index 0000000000..4580ac0f31
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home
+
+import im.vector.app.core.time.Clock
+import im.vector.app.features.settings.VectorPreferences
+import javax.inject.Inject
+
+class SetUnverifiedSessionsAlertShownUseCase @Inject constructor(
+        private val vectorPreferences: VectorPreferences,
+        private val clock: Clock,
+) {
+
+    fun execute(deviceIds: List) {
+        val epochMillis = clock.epochMillis()
+        deviceIds.forEach {
+            vectorPreferences.setUnverifiedSessionsAlertLastShownMillis(it, epochMillis)
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt
index 0455b4399a..18c7ed9689 100644
--- a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt
@@ -28,9 +28,11 @@ class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor(
         private val clock: Clock,
 ) {
 
-    fun execute(): Boolean {
+    fun execute(deviceId: String?): Boolean {
+        deviceId ?: return false
+
         val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled()
-        val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis()
+        val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis(deviceId)
         return isUnverifiedSessionsAlertEnabled &&
                 clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
index 855c47f4bb..21c7bd6ea1 100644
--- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
@@ -19,7 +19,6 @@ package im.vector.app.features.home
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.MavericksState
 import com.airbnb.mvrx.MavericksViewModelFactory
-import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import dagger.assisted.Assisted
@@ -33,7 +32,6 @@ import im.vector.app.core.platform.EmptyViewEvents
 import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.platform.VectorViewModelAction
 import im.vector.app.core.time.Clock
-import im.vector.app.features.settings.VectorPreferences
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
@@ -63,12 +61,16 @@ data class DeviceDetectionInfo(
 class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
         @Assisted initialState: UnknownDevicesState,
         session: Session,
-        private val vectorPreferences: VectorPreferences,
         clock: Clock,
+        private val shouldShowUnverifiedSessionsAlertUseCase: ShouldShowUnverifiedSessionsAlertUseCase,
+        private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase,
+        private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase,
+        private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase,
 ) : VectorViewModel(initialState) {
 
     sealed class Action : VectorViewModelAction {
         data class IgnoreDevice(val deviceIds: List) : Action()
+        data class IgnoreNewLogin(val deviceIds: List) : Action()
     }
 
     @AssistedFactory
@@ -86,8 +88,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
         }
     }
 
-    private val ignoredDeviceList = ArrayList()
-
     init {
         val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId)
                 .firstOrNull { it.deviceId == session.sessionParams.deviceId }
@@ -95,12 +95,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
                 ?: clock.epochMillis()
         Timber.v("## Detector - Current Session first time seen $currentSessionTs")
 
-        ignoredDeviceList.addAll(
-                vectorPreferences.getUnknownDeviceDismissedList().also {
-                    Timber.v("## Detector - Remembered ignored list $it")
-                }
-        )
-
         combine(
                 session.flow().liveUserCryptoDevices(session.myUserId),
                 session.flow().liveMyDevicesInfo(),
@@ -114,13 +108,15 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
                         cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
                     }
                     // filter out ignored devices
-                    .filter { !ignoredDeviceList.contains(it.deviceId) }
+                    .filter { shouldShowUnverifiedSessionsAlertUseCase.execute(it.deviceId) }
                     .sortedByDescending { it.lastSeenTs }
                     .map { deviceInfo ->
                         val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0
+                        val isNew = isNewLoginAlertShownUseCase.execute(deviceInfo.deviceId).not() && deviceKnownSince > currentSessionTs
+
                         DeviceDetectionInfo(
                                 deviceInfo,
-                                deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive,
+                                isNew,
                                 pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change
                         )
                     }
@@ -150,22 +146,11 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
     override fun handle(action: Action) {
         when (action) {
             is Action.IgnoreDevice -> {
-                ignoredDeviceList.addAll(action.deviceIds)
-                // local echo
-                withState { state ->
-                    state.unknownSessions.invoke()?.let { detectedSessions ->
-                        val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) }
-                        setState {
-                            copy(unknownSessions = Success(updated))
-                        }
-                    }
-                }
+                setUnverifiedSessionsAlertShownUseCase.execute(action.deviceIds)
+            }
+            is Action.IgnoreNewLogin -> {
+                setNewLoginAlertShownUseCase.execute(action.deviceIds)
             }
         }
     }
-
-    override fun onCleared() {
-        vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
-        super.onCleared()
-    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index 3f35080057..e1457b0ebf 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -227,8 +227,6 @@ class VectorPreferences @Inject constructor(
         private const val MEDIA_SAVING_1_MONTH = 2
         private const val MEDIA_SAVING_FOREVER = 3
 
-        private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST"
-
         private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
 
         private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION"
@@ -245,7 +243,8 @@ class VectorPreferences @Inject constructor(
         // This key will be used to enable user for displaying live user info or not.
         const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO"
 
-        const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS"
+        const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS_"
+        const val SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE = "SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE_"
 
         // Possible values for TAKE_PHOTO_VIDEO_MODE
         const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
@@ -522,18 +521,6 @@ class VectorPreferences @Inject constructor(
         return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true)
     }
 
-    fun storeUnknownDeviceDismissedList(deviceIds: List) {
-        defaultPrefs.edit(true) {
-            putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet())
-        }
-    }
-
-    fun getUnknownDeviceDismissedList(): List {
-        return tryOrNull {
-            defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList()
-        }.orEmpty()
-    }
-
     /**
      * Update the notification ringtone.
      *
@@ -1244,13 +1231,23 @@ class VectorPreferences @Inject constructor(
         }
     }
 
-    fun getUnverifiedSessionsAlertLastShownMillis(): Long {
-        return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, 0)
+    fun getUnverifiedSessionsAlertLastShownMillis(deviceId: String): Long {
+        return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, 0)
+    }
+
+    fun setUnverifiedSessionsAlertLastShownMillis(deviceId: String, lastShownMillis: Long) {
+        defaultPrefs.edit {
+            putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, lastShownMillis)
+        }
+    }
+
+    fun isNewLoginAlertShownForDevice(deviceId: String): Boolean {
+        return defaultPrefs.getBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, false)
     }
 
-    fun setUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) {
+    fun setNewLoginAlertShownForDevice(deviceId: String) {
         defaultPrefs.edit {
-            putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS, lastShownMillis)
+            putBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, true)
         }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt
index cb4b8b2a1f..5d08499e32 100644
--- a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt
@@ -23,7 +23,8 @@ import im.vector.app.test.fakes.FakeVectorPreferences
 import org.amshove.kluent.shouldBe
 import org.junit.Test
 
-private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS
+private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS.toLong()
+private const val A_DEVICE_ID = "A_DEVICE_ID"
 
 class ShouldShowUnverifiedSessionsAlertUseCaseTest {
 
@@ -42,7 +43,7 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest {
         fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(false)
         fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L)
 
-        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false
+        shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false
     }
 
     @Test
@@ -51,7 +52,7 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest {
         fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L)
         fakeClock.givenEpoch(AN_EPOCH + 1)
 
-        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true
+        shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true
     }
 
     @Test
@@ -60,7 +61,7 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest {
         fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH)
         fakeClock.givenEpoch(AN_EPOCH * 2 + 1)
 
-        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe true
+        shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true
     }
 
     @Test
@@ -69,6 +70,6 @@ class ShouldShowUnverifiedSessionsAlertUseCaseTest {
         fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH)
         fakeClock.givenEpoch(AN_EPOCH + 1)
 
-        shouldShowUnverifiedSessionsAlertUseCase.execute() shouldBe false
+        shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false
     }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 101657b260..77df3ffc7a 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -58,6 +58,6 @@ class FakeVectorPreferences {
     }
 
     fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) {
-        every { instance.getUnverifiedSessionsAlertLastShownMillis() } returns lastShownMillis
+        every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis
     }
 }

From 6f934e2d49e77f949b0e1c0eaae3651fabaac88b Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Fri, 2 Dec 2022 16:50:45 +0100
Subject: [PATCH 435/679] Extract paparazzi rule creation

---
 .../PaparazziExampleScreenshotTest.kt         | 16 +--------
 .../im/vector/app/screenshot/PaparazziRule.kt | 35 +++++++++++++++++++
 2 files changed, 36 insertions(+), 15 deletions(-)
 create mode 100644 vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt

diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt
index 65f89dcc6a..58658651cf 100644
--- a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt
+++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt
@@ -16,14 +16,9 @@
 
 package im.vector.app.screenshot
 
-import android.os.Build
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintLayout
-import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3
-import app.cash.paparazzi.Paparazzi
-import app.cash.paparazzi.androidHome
-import app.cash.paparazzi.detectEnvironment
 import im.vector.app.R
 import org.junit.Rule
 import org.junit.Test
@@ -31,16 +26,7 @@ import org.junit.Test
 class PaparazziExampleScreenshotTest {
 
     @get:Rule
-    val paparazzi = Paparazzi(
-            // Apply trick from https://github.com/cashapp/paparazzi/issues/489#issuecomment-1195674603
-            environment = detectEnvironment().copy(
-                    platformDir = "${androidHome()}/platforms/android-32",
-                    compileSdkVersion = Build.VERSION_CODES.S_V2 /* 32 */
-            ),
-            deviceConfig = PIXEL_3,
-            theme = "Theme.Vector.Light",
-            maxPercentDifference = 0.0,
-    )
+    val paparazzi = createPaparazziRule()
 
     @Test
     fun `example paparazzi test`() {
diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt
new file mode 100644
index 0000000000..970bb15a25
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.screenshot
+
+import android.os.Build
+import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3
+import app.cash.paparazzi.Paparazzi
+import app.cash.paparazzi.androidHome
+import app.cash.paparazzi.detectEnvironment
+
+fun createPaparazziRule() = Paparazzi(
+        // Apply trick from https://github.com/cashapp/paparazzi/issues/489#issuecomment-1195674603
+        environment = detectEnvironment().copy(
+                platformDir = "${androidHome()}/platforms/android-32",
+                compileSdkVersion = Build.VERSION_CODES.S_V2 /* 32 */
+        ),
+        deviceConfig = PIXEL_3,
+        theme = "Theme.Vector.Light",
+        maxPercentDifference = 0.0,
+)
+

From e857407bc1dd444bbe77fa7ff7e6399171a58cd3 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 16:50:46 +0100
Subject: [PATCH 436/679] Adding changelog entry

---
 changelog.d/7693.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7693.feature

diff --git a/changelog.d/7693.feature b/changelog.d/7693.feature
new file mode 100644
index 0000000000..271964db82
--- /dev/null
+++ b/changelog.d/7693.feature
@@ -0,0 +1 @@
+[Session manager] Add action to signout all the other session

From f576f833391edc677b4526e88059e5d3ba62eef7 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 2 Dec 2022 19:02:55 +0300
Subject: [PATCH 437/679] Add changelog.

---
 changelog.d/7694.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7694.feature

diff --git a/changelog.d/7694.feature b/changelog.d/7694.feature
new file mode 100644
index 0000000000..408925974e
--- /dev/null
+++ b/changelog.d/7694.feature
@@ -0,0 +1 @@
+Remind unverified sessions with a banner once a week

From ab43f4cf14fdfb0781d9afb3d3651b93605208d3 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Fri, 2 Dec 2022 17:02:45 +0100
Subject: [PATCH 438/679] Add snapshot test for room item

---
 .../app/screenshot/RoomItemScreenshotTest.kt  | 66 +++++++++++++++++++
 ..._RoomItemScreenshotTest_item room test.png |  3 +
 ..._item room two line and highlight test.png |  3 +
 3 files changed, 72 insertions(+)
 create mode 100644 vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt
 create mode 100644 vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png
 create mode 100644 vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png

diff --git a/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt
new file mode 100644
index 0000000000..d1f4034f43
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.screenshot
+
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import im.vector.app.R
+import im.vector.app.features.home.room.list.UnreadCounterBadgeView
+import org.junit.Rule
+import org.junit.Test
+
+class RoomItemScreenshotTest {
+
+    @get:Rule
+    val paparazzi = createPaparazziRule()
+
+    @Test
+    fun `item room test`() {
+        val view = paparazzi.inflate(R.layout.item_room)
+
+        view.findViewById(R.id.roomUnreadIndicator).isVisible = true
+        view.findViewById(R.id.roomNameView).text = "Room name"
+        view.findViewById(R.id.roomLastEventTimeView).text = "12:34"
+        view.findViewById(R.id.subtitleView).text = "Latest message"
+        view.findViewById(R.id.roomDraftBadge).isVisible = true
+        view.findViewById(R.id.roomUnreadCounterBadgeView).let {
+            it.isVisible = true
+            it.render(UnreadCounterBadgeView.State.Count(8, false))
+        }
+
+        paparazzi.snapshot(view)
+    }
+
+    @Test
+    fun `item room two line and highlight test`() {
+        val view = paparazzi.inflate(R.layout.item_room)
+
+        view.findViewById(R.id.roomUnreadIndicator).isVisible = true
+        view.findViewById(R.id.roomNameView).text = "Room name"
+        view.findViewById(R.id.roomLastEventTimeView).text = "23:59"
+        view.findViewById(R.id.subtitleView).text = "Latest message\nOn two lines"
+        view.findViewById(R.id.roomDraftBadge).isVisible = true
+        view.findViewById(R.id.roomUnreadCounterBadgeView).let {
+            it.isVisible = true
+            it.render(UnreadCounterBadgeView.State.Count(88, true))
+        }
+
+        paparazzi.snapshot(view)
+    }
+}
diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png
new file mode 100644
index 0000000000..1e87449b3c
--- /dev/null
+++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d33e82c6647bab9dcb3745d8c5a5448d60049279c365b9f64816eb9c958360d2
+size 15015
diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png
new file mode 100644
index 0000000000..83fcb8d000
--- /dev/null
+++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:91a106e2a3f7310ac05425a2413ccec0aaa07720609d77a2ecd9a9d0d602b296
+size 17232

From 62e2f06e2a4f73bf6de19062f979481979cdc95b Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 17:08:29 +0100
Subject: [PATCH 439/679] Adding menu for current session header

---
 library/ui-strings/src/main/res/values/strings.xml   |  1 +
 .../devices/v2/VectorSettingsDevicesFragment.kt      | 11 +++++++----
 .../main/res/layout/fragment_settings_devices.xml    |  1 +
 .../main/res/menu/menu_current_session_header.xml    | 12 ++++++++++++
 4 files changed, 21 insertions(+), 4 deletions(-)
 create mode 100644 vector/src/main/res/menu/menu_current_session_header.xml

diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 58fc62b347..684bc6f7b2 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3359,6 +3359,7 @@
         Sign out of %1$d session
         Sign out of %1$d sessions
     
+    Sign out of all other sessions
     Show IP address
     Hide IP address
     Sign out of this session
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index b27d8a7270..c371581395 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -247,7 +247,7 @@ class VectorSettingsDevicesFragment :
             val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId }
 
             renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified)
-            renderCurrentDevice(currentDeviceInfo)
+            renderCurrentSessionView(currentDeviceInfo)
             renderOtherSessionsView(otherDevices, state.isShowingIpAddress)
         } else {
             hideSecurityRecommendations()
@@ -310,11 +310,11 @@ class VectorSettingsDevicesFragment :
             hideOtherSessionsView()
         } else {
             views.deviceListHeaderOtherSessions.isVisible = true
-            val color = colorProvider.getColorFromAttribute(R.attr.colorError)
+            val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
             val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout)
             val nbDevices = otherDevices.size
             multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
-            multiSignoutItem.setTextColor(color)
+            multiSignoutItem.setTextColor(colorDestructive)
             views.deviceListOtherSessions.isVisible = true
             val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) }
             views.deviceListOtherSessions.render(
@@ -335,9 +335,12 @@ class VectorSettingsDevicesFragment :
         views.deviceListOtherSessions.isVisible = false
     }
 
-    private fun renderCurrentDevice(currentDeviceInfo: DeviceFullInfo?) {
+    private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?) {
         currentDeviceInfo?.let {
             views.deviceListHeaderCurrentSession.isVisible = true
+            val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
+            val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
+            signoutOtherSessionsItem.setTextColor(colorDestructive)
             views.deviceListCurrentSession.isVisible = true
             val viewState = SessionInfoViewState(
                     isCurrentSession = true,
diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml
index 8134774887..731049f3a2 100644
--- a/vector/src/main/res/layout/fragment_settings_devices.xml
+++ b/vector/src/main/res/layout/fragment_settings_devices.xml
@@ -67,6 +67,7 @@
             app:layout_constraintTop_toBottomOf="@id/deviceListSecurityRecommendationsDivider"
             app:sessionsListHeaderDescription=""
             app:sessionsListHeaderHasLearnMoreLink="false"
+            app:sessionsListHeaderMenu="@menu/menu_current_session_header"
             app:sessionsListHeaderTitle="@string/device_manager_current_session_title" />
 
         
+
+
+    
+
+

From 2b8dc13dcad821517cdee725895762fd39c59676 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 17:11:10 +0100
Subject: [PATCH 440/679] Adding listener on the new menu item

---
 .../devices/v2/VectorSettingsDevicesFragment.kt   | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index c371581395..a208362680 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -99,6 +99,7 @@ class VectorSettingsDevicesFragment :
         super.onViewCreated(view, savedInstanceState)
 
         initWaitingView()
+        initCurrentSessionHeaderView()
         initOtherSessionsHeaderView()
         initOtherSessionsView()
         initSecurityRecommendationsView()
@@ -139,6 +140,18 @@ class VectorSettingsDevicesFragment :
         views.waitingView.waitingStatusText.isVisible = true
     }
 
+    private fun initCurrentSessionHeaderView() {
+        views.deviceListHeaderCurrentSession.setOnMenuItemClickListener { menuItem ->
+            when (menuItem.itemId) {
+                R.id.currentSessionHeaderSignoutOtherSessions -> {
+                    confirmMultiSignoutOtherSessions()
+                    true
+                }
+                else -> false
+            }
+        }
+    }
+
     private fun initOtherSessionsHeaderView() {
         views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem ->
             when (menuItem.itemId) {
@@ -327,7 +340,7 @@ class VectorSettingsDevicesFragment :
             } else {
                 stringProvider.getString(R.string.device_manager_other_sessions_show_ip_address)
             }
-         }
+        }
     }
 
     private fun hideOtherSessionsView() {

From efc436c3f5b878b115f75b88b3d3caec7896b70c Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 17:17:44 +0100
Subject: [PATCH 441/679] Hide the action when there are no other sessions

---
 .../settings/devices/v2/VectorSettingsDevicesFragment.kt    | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index a208362680..d748600416 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -53,6 +53,7 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie
 import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
 import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
 import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import javax.inject.Inject
 
@@ -260,7 +261,7 @@ class VectorSettingsDevicesFragment :
             val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId }
 
             renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified)
-            renderCurrentSessionView(currentDeviceInfo)
+            renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse())
             renderOtherSessionsView(otherDevices, state.isShowingIpAddress)
         } else {
             hideSecurityRecommendations()
@@ -348,12 +349,13 @@ class VectorSettingsDevicesFragment :
         views.deviceListOtherSessions.isVisible = false
     }
 
-    private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?) {
+    private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) {
         currentDeviceInfo?.let {
             views.deviceListHeaderCurrentSession.isVisible = true
             val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
             val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
             signoutOtherSessionsItem.setTextColor(colorDestructive)
+            signoutOtherSessionsItem.isVisible = hasOtherDevices
             views.deviceListCurrentSession.isVisible = true
             val viewState = SessionInfoViewState(
                     isCurrentSession = true,

From b8ab1b5620e071974d3600479b915e91ba72780c Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 17:35:13 +0100
Subject: [PATCH 442/679] Adding changelog entry

---
 changelog.d/7697.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7697.feature

diff --git a/changelog.d/7697.feature b/changelog.d/7697.feature
new file mode 100644
index 0000000000..6d71a84a40
--- /dev/null
+++ b/changelog.d/7697.feature
@@ -0,0 +1 @@
+[Session manager] Add actions to rename and signout current session

From 980d59ab58caec7629ec0110fcf9236ccf100ba3 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 2 Dec 2022 21:21:12 +0300
Subject: [PATCH 443/679] Fix lint.

---
 library/ui-strings/src/main/res/values/strings.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 3945a80393..683b9f754d 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -2648,9 +2648,9 @@
     Encrypted by an unverified device
     The authenticity of this encrypted message can\'t be guaranteed on this device.
     
-    Review where you’re logged in
+    Review where you’re logged in
     
-    Verify all your sessions to ensure your account & messages are safe
+    Verify all your sessions to ensure your account & messages are safe
     You have unverified sessions
     Review to ensure your account is safe
     

From 53ef97d9492c3aa674246918bdd8072f9be69b02 Mon Sep 17 00:00:00 2001
From: phardyle 
Date: Fri, 2 Dec 2022 10:05:03 +0000
Subject: [PATCH 444/679] Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (2550 of 2558 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hans/
---
 .../src/main/res/values-zh-rCN/strings.xml    | 23 +++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml
index 5ab8a351d1..0a01610c36 100644
--- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml
+++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml
@@ -1007,7 +1007,7 @@
     您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。
     同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。
     启用详细日志。
-    详细日志将通过在您发送 RageShake 时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。
+    详细日志将通过在您发送愤怒摇动(RageShake)时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。
     接收你的主服务器条款和条件后请重试。
     服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。
     发送附件
@@ -1205,7 +1205,7 @@
     高级设置
     开发者模式
     开发者模式激活隐藏的功能,也可能使应用不稳定。仅供开发者使用!
-    摇一摇
+    愤怒摇动(Rageshake)
     检测阈值
     摇动手机以测试检测阈值
     检测到摇动!
@@ -1213,7 +1213,7 @@
     当前会话
     其它会话
     仅显示第一个结果,请输入更多字符…
-    快速失败
+    快速失败(Fail-fast)
     发生意外错误时,${app_name} 可能更经常崩溃
     在明文消息前添加 ¯\\_(ツ)_/¯
     启用加密
@@ -2694,7 +2694,7 @@
     验证您当前的会话以显示此会话的验证状态。
     未知的验证状态
     开始语音广播
-    缓冲
+    正在缓冲……
     暂停语音广播
     实时
     知道了
@@ -2789,4 +2789,19 @@
     
         已选择 %1$d
     
+    已创建投票。
+    已发送贴纸。
+    已发送视频。
+    已发送图片。
+    已发送语音消息。
+    已发送音频文件。
+    已发送文件。
+    已验证的会话是在输入你的口令词组或用另一个已验证的会话确认你的身份之后你使用此账户的任何地方。
+\n
+\n这意味着你拥有解锁你的已加密消息和向其他用户证明你信任此会话所需的全部密钥。
+    
+        登出%1$d个会话
+    
+    登出
+    剩余%1$s
 
\ No newline at end of file

From 6d1f9408c87d2b05de052307f2994300297d996c Mon Sep 17 00:00:00 2001
From: Vri 
Date: Fri, 2 Dec 2022 05:54:40 +0000
Subject: [PATCH 445/679] Translated using Weblate (German)

Currently translated at 100.0% (83 of 83 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/de/
---
 fastlane/metadata/android/de-DE/changelogs/40105100.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/de-DE/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/de-DE/changelogs/40105100.txt b/fastlane/metadata/android/de-DE/changelogs/40105100.txt
new file mode 100644
index 0000000000..de5f4d90e8
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Die wichtigsten Änderungen in dieser Version: Der Vollbildmodus des Textverarbeitungseditors wurde neu umgesetzt und es wurden diverse Fehler behoben.
+Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases

From 098dee1aa7a55d47107ef91cdcb4e2664eac0ab9 Mon Sep 17 00:00:00 2001
From: Jozef Gaal 
Date: Thu, 1 Dec 2022 21:32:42 +0000
Subject: [PATCH 446/679] Translated using Weblate (Slovak)

Currently translated at 100.0% (83 of 83 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/
---
 fastlane/metadata/android/sk/changelogs/40105100.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/sk/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/sk/changelogs/40105100.txt b/fastlane/metadata/android/sk/changelogs/40105100.txt
new file mode 100644
index 0000000000..c286f155d4
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Hlavné zmeny v tejto verzii: Nová implementácia celo-obrazovkového režimu pre Rozšírený textový editor a opravy chýb.
+Úplný zoznam zmien: https://github.com/vector-im/element-android/releases

From 873fa2a210e299bc02fe0a4bf0b34d3662424df6 Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk 
Date: Thu, 1 Dec 2022 09:57:42 +0000
Subject: [PATCH 447/679] Translated using Weblate (Ukrainian)

Currently translated at 100.0% (83 of 83 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/
---
 fastlane/metadata/android/uk/changelogs/40105100.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/uk/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/uk/changelogs/40105100.txt b/fastlane/metadata/android/uk/changelogs/40105100.txt
new file mode 100644
index 0000000000..6bb3ab95c7
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Основні зміни в цій версії: Нова реалізація повноекранного режиму для редактора розширеного тексту та виправлення помилок.
+Перелік усіх змін: https://github.com/vector-im/element-android/releases

From 10d03e16afb7e291bad83bf0fd0cf98de23ea7d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= 
Date: Thu, 1 Dec 2022 16:57:40 +0000
Subject: [PATCH 448/679] Translated using Weblate (Estonian)

Currently translated at 100.0% (83 of 83 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/et/
---
 fastlane/metadata/android/et/changelogs/40105100.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/et/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/et/changelogs/40105100.txt b/fastlane/metadata/android/et/changelogs/40105100.txt
new file mode 100644
index 0000000000..f6212db01b
--- /dev/null
+++ b/fastlane/metadata/android/et/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Põhilised muutused selles versioonis: tekstitoimeti täisekraanivaade ja erinevate vigade parandused.
+Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases

From 61eb0d6b449ae62e7b50708c745d3b627274ac9b Mon Sep 17 00:00:00 2001
From: Jeff Huang 
Date: Fri, 2 Dec 2022 01:49:09 +0000
Subject: [PATCH 449/679] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (83 of 83 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/
---
 fastlane/metadata/android/zh-TW/changelogs/40105100.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/zh-TW/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105100.txt b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt
new file mode 100644
index 0000000000..20341b84fe
--- /dev/null
+++ b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。
+完整的變更紀錄:https://github.com/vector-im/element-android/releases

From 58d10e901e689a53803ad6cb3fe0c9f796d68fc1 Mon Sep 17 00:00:00 2001
From: waclaw66 
Date: Thu, 1 Dec 2022 11:18:34 +0000
Subject: [PATCH 450/679] Translated using Weblate (Czech)

Currently translated at 100.0% (83 of 83 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/
---
 fastlane/metadata/android/cs-CZ/changelogs/40105100.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt
new file mode 100644
index 0000000000..8c51742e06
--- /dev/null
+++ b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Hlavní změny v této verzi: Nová implementace celoobrazovkového režimu pro editor formátovaného textu a opravy chyb.
+Úplný seznam změn: https://github.com/vector-im/element-android/releases

From 70e9f13ec23f4c17a65d1c1bf85689e8ddd59ac1 Mon Sep 17 00:00:00 2001
From: Linerly 
Date: Thu, 1 Dec 2022 12:00:15 +0000
Subject: [PATCH 451/679] Translated using Weblate (Indonesian)

Currently translated at 100.0% (83 of 83 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/id/
---
 fastlane/metadata/android/id/changelogs/40105100.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/id/changelogs/40105100.txt

diff --git a/fastlane/metadata/android/id/changelogs/40105100.txt b/fastlane/metadata/android/id/changelogs/40105100.txt
new file mode 100644
index 0000000000..0c7d2f5262
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/40105100.txt
@@ -0,0 +1,2 @@
+Perubahan utama dalam versi ini: Penerapan baru mode layar penuh untuk Penyunting Teks Kaya dan perbaikan kutu.
+Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases

From 9456789047f96d0fd2aa2313a2e2ff97a96f91da Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Mon, 28 Nov 2022 11:38:06 +0100
Subject: [PATCH 452/679] Adding changelog entry

---
 changelog.d/7653.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7653.bugfix

diff --git a/changelog.d/7653.bugfix b/changelog.d/7653.bugfix
new file mode 100644
index 0000000000..ae49c4ed4e
--- /dev/null
+++ b/changelog.d/7653.bugfix
@@ -0,0 +1 @@
+ANR when asking to select the notification method

From 4dbca7858cbb49d95135a34e2fc4f84330d75460 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Tue, 29 Nov 2022 15:07:26 +0100
Subject: [PATCH 453/679] Adding new use cases to handle the Unified push
 registration

---
 .../im/vector/app/push/fcm/FdroidFcmHelper.kt |  3 +-
 .../im/vector/app/push/fcm/GoogleFcmHelper.kt | 13 ++--
 .../EnsureFcmTokenIsRetrievedUseCase.kt       | 44 ++++++++++++
 .../im/vector/app/core/pushers/FcmHelper.kt   |  3 +-
 .../pushers/RegisterUnifiedPushUseCase.kt     | 70 +++++++++++++++++++
 .../app/core/pushers/UnifiedPushHelper.kt     | 60 ++++++++++++++--
 .../pushers/UnregisterUnifiedPushUseCase.kt   | 51 ++++++++++++++
 .../vector/app/features/home/HomeActivity.kt  | 29 ++------
 .../features/home/HomeActivityViewActions.kt  |  1 +
 .../features/home/HomeActivityViewEvents.kt   |  3 +
 .../features/home/HomeActivityViewModel.kt    | 53 +++++++-------
 .../features/home/HomeActivityViewState.kt    |  1 -
 ...leNotificationsForCurrentSessionUseCase.kt | 12 ++--
 ...rSettingsNotificationPreferenceFragment.kt | 10 +--
 14 files changed, 271 insertions(+), 82 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
 create mode 100644 vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
 create mode 100644 vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt

diff --git a/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt
index 5b83769116..44fd92953e 100755
--- a/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt
+++ b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt
@@ -17,7 +17,6 @@
 
 package im.vector.app.push.fcm
 
-import android.app.Activity
 import android.content.Context
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.FcmHelper
@@ -44,7 +43,7 @@ class FdroidFcmHelper @Inject constructor(
         // No op
     }
 
-    override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
+    override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
         // No op
     }
 
diff --git a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt
index 7cf90cf874..53e65f88b4 100755
--- a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt
+++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt
@@ -15,7 +15,6 @@
  */
 package im.vector.app.push.fcm
 
-import android.app.Activity
 import android.content.Context
 import android.content.SharedPreferences
 import android.widget.Toast
@@ -23,6 +22,7 @@ import androidx.core.content.edit
 import com.google.android.gms.common.ConnectionResult
 import com.google.android.gms.common.GoogleApiAvailability
 import com.google.firebase.messaging.FirebaseMessaging
+import dagger.hilt.android.qualifiers.ApplicationContext
 import im.vector.app.R
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.di.DefaultPreferences
@@ -36,8 +36,8 @@ import javax.inject.Inject
  * It has an alter ego in the fdroid variant.
  */
 class GoogleFcmHelper @Inject constructor(
-        @DefaultPreferences
-        private val sharedPrefs: SharedPreferences,
+        @ApplicationContext private val context: Context,
+        @DefaultPreferences private val sharedPrefs: SharedPreferences,
 ) : FcmHelper {
     companion object {
         private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
@@ -56,10 +56,9 @@ class GoogleFcmHelper @Inject constructor(
         }
     }
 
-    override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
-        //        if (TextUtils.isEmpty(getFcmToken(activity))) {
+    override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
         // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
-        if (checkPlayServices(activity)) {
+        if (checkPlayServices(context)) {
             try {
                 FirebaseMessaging.getInstance().token
                         .addOnSuccessListener { token ->
@@ -75,7 +74,7 @@ class GoogleFcmHelper @Inject constructor(
                 Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
             }
         } else {
-            Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
+            Toast.makeText(context, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
             Timber.e("No valid Google Play Services found. Cannot use FCM.")
         }
     }
diff --git a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
new file mode 100644
index 0000000000..4a8ff5fb39
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.pushers
+
+import im.vector.app.core.di.ActiveSessionHolder
+import timber.log.Timber
+import javax.inject.Inject
+
+class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
+        private val unifiedPushHelper: UnifiedPushHelper,
+        private val fcmHelper: FcmHelper,
+        private val activeSessionHolder: ActiveSessionHolder,
+) {
+
+    // TODO add unit tests
+    fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
+        if (unifiedPushHelper.isEmbeddedDistributor()) {
+            Timber.d("ensureFcmTokenIsRetrieved")
+            fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
+        }
+    }
+
+    private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
+        val currentSession = activeSessionHolder.getActiveSession()
+        val currentPushers = currentSession.pushersService().getPushers()
+        currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
+    } else {
+        false
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt
index 7b2c5e3959..0cc251ce31 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt
@@ -39,11 +39,10 @@ interface FcmHelper {
     /**
      * onNewToken may not be called on application upgrade, so ensure my shared pref is set.
      *
-     * @param activity the first launch Activity.
      * @param pushersManager the instance to register the pusher on.
      * @param registerPusher whether the pusher should be registered.
      */
-    fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean)
+    fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean)
 
     fun onEnterForeground(activeSessionHolder: ActiveSessionHolder)
 
diff --git a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
new file mode 100644
index 0000000000..7aafa07348
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.pushers
+
+import android.content.Context
+import dagger.hilt.android.qualifiers.ApplicationContext
+import im.vector.app.features.VectorFeatures
+import org.unifiedpush.android.connector.UnifiedPush
+import javax.inject.Inject
+
+class RegisterUnifiedPushUseCase @Inject constructor(
+        @ApplicationContext private val context: Context,
+        private val vectorFeatures: VectorFeatures,
+) {
+
+    sealed interface RegisterUnifiedPushResult {
+        object Success : RegisterUnifiedPushResult
+        data class NeedToAskUserForDistributor(val distributors: List) : RegisterUnifiedPushResult
+    }
+
+    // TODO add unit tests
+    fun execute(distributor: String = ""): RegisterUnifiedPushResult {
+        if(distributor.isNotEmpty()) {
+            saveAndRegisterApp(distributor)
+            return RegisterUnifiedPushResult.Success
+        }
+
+        if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
+            saveAndRegisterApp(context.packageName)
+            return RegisterUnifiedPushResult.Success
+        }
+
+        if (UnifiedPush.getDistributor(context).isNotEmpty()) {
+            registerApp()
+            return RegisterUnifiedPushResult.Success
+        }
+
+        val distributors = UnifiedPush.getDistributors(context)
+
+        return if (distributors.size == 1) {
+            saveAndRegisterApp(distributors.first())
+            RegisterUnifiedPushResult.Success
+        } else {
+            RegisterUnifiedPushResult.NeedToAskUserForDistributor(distributors)
+        }
+    }
+
+    private fun saveAndRegisterApp(distributor: String) {
+        UnifiedPush.saveDistributor(context, distributor)
+        registerApp()
+    }
+
+    private fun registerApp() {
+        UnifiedPush.registerApp(context)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
index aab94eca93..64d2a79494 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
@@ -17,6 +17,7 @@
 package im.vector.app.core.pushers
 
 import android.content.Context
+import androidx.annotation.MainThread
 import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.lifecycleScope
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -28,7 +29,9 @@ import im.vector.app.core.utils.getApplicationLabel
 import im.vector.app.features.VectorFeatures
 import im.vector.app.features.settings.BackgroundSyncMode
 import im.vector.app.features.settings.VectorPreferences
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.Matrix
 import org.matrix.android.sdk.api.cache.CacheStrategy
 import org.matrix.android.sdk.api.util.MatrixJsonParser
@@ -49,6 +52,7 @@ class UnifiedPushHelper @Inject constructor(
 
     // Called when the home activity starts
     // or when notifications are enabled
+    // TODO remove and replace by use case
     fun register(
             activity: FragmentActivity,
             onDoneRunnable: Runnable? = null,
@@ -66,10 +70,11 @@ class UnifiedPushHelper @Inject constructor(
     // The registration is forced in 2 cases :
     // * in the settings
     // * in the troubleshoot list (doFix)
+    // TODO remove and replace by use case
     fun forceRegister(
             activity: FragmentActivity,
             pushersManager: PushersManager,
-            onDoneRunnable: Runnable? = null
+            @MainThread onDoneRunnable: Runnable? = null
     ) {
         registerInternal(
                 activity,
@@ -79,17 +84,21 @@ class UnifiedPushHelper @Inject constructor(
         )
     }
 
+    // TODO remove
     private fun registerInternal(
             activity: FragmentActivity,
             force: Boolean = false,
             pushersManager: PushersManager? = null,
             onDoneRunnable: Runnable? = null
     ) {
-        activity.lifecycleScope.launch {
+        activity.lifecycleScope.launch(Dispatchers.IO) {
+            Timber.d("registerInternal force=$force, $activity on thread ${Thread.currentThread()}")
             if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
                 UnifiedPush.saveDistributor(context, context.packageName)
                 UnifiedPush.registerApp(context)
-                onDoneRunnable?.run()
+                withContext(Dispatchers.Main) {
+                    onDoneRunnable?.run()
+                }
                 return@launch
             }
             if (force) {
@@ -99,7 +108,9 @@ class UnifiedPushHelper @Inject constructor(
             // the !force should not be needed
             if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) {
                 UnifiedPush.registerApp(context)
-                onDoneRunnable?.run()
+                withContext(Dispatchers.Main) {
+                    onDoneRunnable?.run()
+                }
                 return@launch
             }
 
@@ -108,7 +119,9 @@ class UnifiedPushHelper @Inject constructor(
             if (!force && distributors.size == 1) {
                 UnifiedPush.saveDistributor(context, distributors.first())
                 UnifiedPush.registerApp(context)
-                onDoneRunnable?.run()
+                withContext(Dispatchers.Main) {
+                    onDoneRunnable?.run()
+                }
             } else {
                 openDistributorDialogInternal(
                         activity = activity,
@@ -164,6 +177,43 @@ class UnifiedPushHelper @Inject constructor(
                 .show()
     }
 
+    @MainThread
+    fun showSelectDistributorDialog(
+            context: Context,
+            distributors: List,
+            onDistributorSelected: (String) -> Unit,
+    ) {
+        val internalDistributorName = stringProvider.getString(
+                if (fcmHelper.isFirebaseAvailable()) {
+                    R.string.unifiedpush_distributor_fcm_fallback
+                } else {
+                    R.string.unifiedpush_distributor_background_sync
+                }
+        )
+
+        val distributorsName = distributors.map {
+            if (it == context.packageName) {
+                internalDistributorName
+            } else {
+                context.getApplicationLabel(it)
+            }
+        }
+
+        MaterialAlertDialogBuilder(context)
+                .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title))
+                .setItems(distributorsName.toTypedArray()) { _, which ->
+                    val distributor = distributors[which]
+                    onDistributorSelected(distributor)
+                }
+                .setOnCancelListener {
+                    // By default, use internal solution (fcm/background sync)
+                    onDistributorSelected(context.packageName)
+                }
+                .setCancelable(true)
+                .show()
+    }
+
+    // TODO remove and replace by use case
     suspend fun unregister(pushersManager: PushersManager? = null) {
         val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
         vectorPreferences.setFdroidSyncBackgroundMode(mode)
diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
new file mode 100644
index 0000000000..d81581679e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.pushers
+
+import android.content.Context
+import dagger.hilt.android.qualifiers.ApplicationContext
+import im.vector.app.features.settings.BackgroundSyncMode
+import im.vector.app.features.settings.VectorPreferences
+import org.unifiedpush.android.connector.UnifiedPush
+import timber.log.Timber
+import javax.inject.Inject
+
+class UnregisterUnifiedPushUseCase @Inject constructor(
+        @ApplicationContext private val context: Context,
+        private val pushersManager: PushersManager,
+        private val vectorPreferences: VectorPreferences,
+        private val unifiedPushStore: UnifiedPushStore,
+        private val unifiedPushHelper: UnifiedPushHelper,
+) {
+
+    // TODO add unit tests
+    suspend fun execute() {
+        val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
+        vectorPreferences.setFdroidSyncBackgroundMode(mode)
+        try {
+            unifiedPushHelper.getEndpointOrToken()?.let {
+                Timber.d("Removing $it")
+                pushersManager.unregisterPusher(it)
+            }
+        } catch (e: Exception) {
+            Timber.d(e, "Probably unregistering a non existing pusher")
+        }
+        unifiedPushStore.storeUpEndpoint(null)
+        unifiedPushStore.storePushGateway(null)
+        UnifiedPush.unregisterApp(context)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
index 2df94fecad..14157a1de8 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
@@ -44,8 +44,6 @@ import im.vector.app.core.extensions.restart
 import im.vector.app.core.extensions.validateBackPressed
 import im.vector.app.core.platform.VectorBaseActivity
 import im.vector.app.core.platform.VectorMenuProvider
-import im.vector.app.core.pushers.FcmHelper
-import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.UnifiedPushHelper
 import im.vector.app.core.utils.registerForPermissionsResult
 import im.vector.app.core.utils.startSharePlainTextIntent
@@ -128,7 +126,6 @@ class HomeActivity :
     private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
 
     @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
-    @Inject lateinit var pushersManager: PushersManager
     @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
     @Inject lateinit var popupAlertManager: PopupAlertManager
     @Inject lateinit var shortcutsHandler: ShortcutsHandler
@@ -137,7 +134,6 @@ class HomeActivity :
     @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
     @Inject lateinit var spaceStateHandler: SpaceStateHandler
     @Inject lateinit var unifiedPushHelper: UnifiedPushHelper
-    @Inject lateinit var fcmHelper: FcmHelper
     @Inject lateinit var nightlyProxy: NightlyProxy
     @Inject lateinit var disclaimerDialog: DisclaimerDialog
     @Inject lateinit var notificationPermissionManager: NotificationPermissionManager
@@ -209,16 +205,6 @@ class HomeActivity :
         isNewAppLayoutEnabled = vectorPreferences.isNewAppLayoutEnabled()
         analyticsScreenName = MobileScreen.ScreenName.Home
         supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
-        unifiedPushHelper.register(this) {
-            if (unifiedPushHelper.isEmbeddedDistributor()) {
-                fcmHelper.ensureFcmTokenIsRetrieved(
-                        this,
-                        pushersManager,
-                        homeActivityViewModel.shouldAddHttpPusher()
-                )
-            }
-        }
-
         sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
         roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java]
         views.drawerLayout.addDrawerListener(drawerListener)
@@ -280,6 +266,7 @@ class HomeActivity :
                 HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes()
                 HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
                 is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
+                is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors)
             }
         }
         homeActivityViewModel.onEach { renderState(it) }
@@ -292,6 +279,12 @@ class HomeActivity :
         homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
     }
 
+    private fun askUserToSelectPushDistributor(distributors: List) {
+        unifiedPushHelper.showSelectDistributorDialog(this, distributors) { selection ->
+            homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection))
+        }
+    }
+
     private fun handleShowNotificationDialog() {
         notificationPermissionManager.eventuallyRequestPermission(this, postPermissionLauncher)
     }
@@ -415,14 +408,6 @@ class HomeActivity :
     }
 
     private fun renderState(state: HomeActivityViewState) {
-        lifecycleScope.launch {
-            if (state.areNotificationsSilenced) {
-                unifiedPushHelper.unregister(pushersManager)
-            } else {
-                unifiedPushHelper.register(this@HomeActivity)
-            }
-        }
-
         when (val status = state.syncRequestState) {
             is SyncRequestState.InitialSyncProgressing -> {
                 val initSyncStepStr = initSyncStepFormatter.format(status.initialSyncStep)
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt
index 5f89c89bc9..54392d5f56 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt
@@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
 sealed interface HomeActivityViewActions : VectorViewModelAction {
     object ViewStarted : HomeActivityViewActions
     object PushPromptHasBeenReviewed : HomeActivityViewActions
+    data class RegisterPushDistributor(val distributor: String) : HomeActivityViewActions
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
index e548fdb2f3..6fdf441d1d 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
@@ -25,9 +25,11 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
             val userItem: MatrixItem.UserItem,
             val waitForIncomingRequest: Boolean = true,
     ) : HomeActivityViewEvents
+
     data class CurrentSessionCannotBeVerified(
             val userItem: MatrixItem.UserItem,
     ) : HomeActivityViewEvents
+
     data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
     object PromptToEnableSessionPush : HomeActivityViewEvents
     object ShowAnalyticsOptIn : HomeActivityViewEvents
@@ -37,4 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
     data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
     object StartRecoverySetupFlow : HomeActivityViewEvents
     data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents
+    data class AskUserForPushDistributor(val distributors: List) : HomeActivityViewEvents
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 49f2079625..3fd555bbea 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -16,7 +16,6 @@
 
 package im.vector.app.features.home
 
-import androidx.lifecycle.asFlow
 import com.airbnb.mvrx.Mavericks
 import com.airbnb.mvrx.MavericksViewModelFactory
 import com.airbnb.mvrx.ViewModelContext
@@ -27,7 +26,9 @@ import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
 import im.vector.app.core.platform.VectorViewModel
-import im.vector.app.features.VectorFeatures
+import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
+import im.vector.app.core.pushers.PushersManager
+import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
 import im.vector.app.features.analytics.AnalyticsConfig
 import im.vector.app.features.analytics.AnalyticsTracker
 import im.vector.app.features.analytics.extensions.toAnalyticsType
@@ -48,12 +49,10 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onCompletion
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.takeWhile
 import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.auth.UserPasswordAuth
@@ -62,11 +61,9 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.raw.RawService
-import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
 import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.getUserOrDefault
 import org.matrix.android.sdk.api.session.pushrules.RuleIds
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -92,8 +89,10 @@ class HomeActivityViewModel @AssistedInject constructor(
         private val analyticsTracker: AnalyticsTracker,
         private val analyticsConfig: AnalyticsConfig,
         private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
-        private val vectorFeatures: VectorFeatures,
         private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
+        private val pushersManager: PushersManager,
+        private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
+        private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
 ) : VectorViewModel(initialState) {
 
     @AssistedFactory
@@ -117,17 +116,32 @@ class HomeActivityViewModel @AssistedInject constructor(
     private fun initialize() {
         if (isInitialized) return
         isInitialized = true
+        registerUnifiedPush(distributor = "")
         cleanupFiles()
         observeInitialSync()
         checkSessionPushIsOn()
         observeCrossSigningReset()
         observeAnalytics()
         observeReleaseNotes()
-        observeLocalNotificationsSilenced()
         initThreadsMigration()
         viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
     }
 
+    private fun registerUnifiedPush(distributor: String) {
+        viewModelScope.launch {
+            when (val result = registerUnifiedPushUseCase.execute(distributor = distributor)) {
+                is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
+                    Timber.d("registerUnifiedPush $distributor need to ask user")
+                    _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor(result.distributors))
+                }
+                RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
+                    Timber.d("registerUnifiedPush $distributor success")
+                    ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
+                }
+            }
+        }
+    }
+
     private fun observeReleaseNotes() = withState { state ->
         if (vectorPreferences.isNewAppLayoutEnabled()) {
             // we don't want to show release notes for new users or after relogin
@@ -146,26 +160,6 @@ class HomeActivityViewModel @AssistedInject constructor(
         }
     }
 
-    fun shouldAddHttpPusher() = if (vectorPreferences.areNotificationEnabledForDevice()) {
-        val currentSession = activeSessionHolder.getActiveSession()
-        val currentPushers = currentSession.pushersService().getPushers()
-        currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
-    } else {
-        false
-    }
-
-    fun observeLocalNotificationsSilenced() {
-        val currentSession = activeSessionHolder.getActiveSession()
-        val deviceId = currentSession.cryptoService().getMyDevice().deviceId
-        viewModelScope.launch {
-            currentSession.accountDataService()
-                    .getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
-                    .asFlow()
-                    .map { it.getOrNull()?.content?.toModel()?.isSilenced ?: false }
-                    .onEach { setState { copy(areNotificationsSilenced = it) } }
-        }
-    }
-
     private fun observeAnalytics() {
         if (analyticsConfig.isEnabled) {
             analyticsStore.didAskUserConsentFlow
@@ -501,6 +495,9 @@ class HomeActivityViewModel @AssistedInject constructor(
             HomeActivityViewActions.ViewStarted -> {
                 initialize()
             }
+            is HomeActivityViewActions.RegisterPushDistributor -> {
+                registerUnifiedPush(distributor = action.distributor)
+            }
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt
index 4df2957cbc..f9c1b37ed5 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt
@@ -23,5 +23,4 @@ import org.matrix.android.sdk.api.session.sync.SyncRequestState
 data class HomeActivityViewState(
         val syncRequestState: SyncRequestState = SyncRequestState.Idle,
         val authenticationDescription: AuthenticationDescription? = null,
-        val areNotificationsSilenced: Boolean = false,
 ) : MavericksState
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
index 180627a15f..91974787bd 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.settings.notifications
 
 import androidx.fragment.app.FragmentActivity
 import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.FcmHelper
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.UnifiedPushHelper
@@ -32,11 +33,12 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
         private val unifiedPushHelper: UnifiedPushHelper,
         private val pushersManager: PushersManager,
-        private val fcmHelper: FcmHelper,
         private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
         private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+        private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
 ) {
 
+    // TODO update unit tests
     suspend fun execute(fragmentActivity: FragmentActivity) {
         val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
         if (pusherForCurrentSession == null) {
@@ -54,13 +56,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
         suspendCoroutine { continuation ->
             try {
                 unifiedPushHelper.register(fragmentActivity) {
-                    if (unifiedPushHelper.isEmbeddedDistributor()) {
-                        fcmHelper.ensureFcmTokenIsRetrieved(
-                                fragmentActivity,
-                                pushersManager,
-                                registerPusher = true
-                        )
-                    }
+                    ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true)
                     continuation.resume(Unit)
                 }
             } catch (error: Exception) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
index 58f86bc949..f8c1a9ad44 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
@@ -37,6 +37,7 @@ import im.vector.app.core.preference.VectorEditTextPreference
 import im.vector.app.core.preference.VectorPreference
 import im.vector.app.core.preference.VectorPreferenceCategory
 import im.vector.app.core.preference.VectorSwitchPreference
+import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.FcmHelper
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.UnifiedPushHelper
@@ -82,6 +83,7 @@ class VectorSettingsNotificationPreferenceFragment :
     @Inject lateinit var notificationPermissionManager: NotificationPermissionManager
     @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase
     @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase
+    @Inject lateinit var ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase
 
     override var titleRes: Int = R.string.settings_notifications
     override val preferenceXmlRes = R.xml.vector_settings_notifications
@@ -183,13 +185,7 @@ class VectorSettingsNotificationPreferenceFragment :
                 it.summary = unifiedPushHelper.getCurrentDistributorName()
                 it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                     unifiedPushHelper.forceRegister(requireActivity(), pushersManager) {
-                        if (unifiedPushHelper.isEmbeddedDistributor()) {
-                            fcmHelper.ensureFcmTokenIsRetrieved(
-                                    requireActivity(),
-                                    pushersManager,
-                                    vectorPreferences.areNotificationEnabledForDevice()
-                            )
-                        }
+                        ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
                         it.summary = unifiedPushHelper.getCurrentDistributorName()
                         session.pushersService().refreshPushers()
                         refreshBackgroundSyncPrefs()

From 2890f41f30d675b9988d12e62bcd94916da04e3c Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Tue, 29 Nov 2022 17:00:44 +0100
Subject: [PATCH 454/679] Replacing unregister method by usecase

---
 .../main/java/im/vector/app/core/di/ActiveSessionHolder.kt | 6 +++---
 .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt   | 2 +-
 .../app/core/pushers/UnregisterUnifiedPushUseCase.kt       | 5 ++---
 .../DisableNotificationsForCurrentSessionUseCase.kt        | 7 ++++---
 4 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt
index f1863cfa23..fead1e15b1 100644
--- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt
+++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt
@@ -19,7 +19,7 @@ package im.vector.app.core.di
 import android.content.Context
 import im.vector.app.ActiveSessionDataSource
 import im.vector.app.core.extensions.startSyncing
-import im.vector.app.core.pushers.UnifiedPushHelper
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.core.services.GuardServiceStarter
 import im.vector.app.core.session.ConfigureAndStartSessionUseCase
 import im.vector.app.features.call.webrtc.WebRtcCallManager
@@ -46,12 +46,12 @@ class ActiveSessionHolder @Inject constructor(
         private val pushRuleTriggerListener: PushRuleTriggerListener,
         private val sessionListener: SessionListener,
         private val imageManager: ImageManager,
-        private val unifiedPushHelper: UnifiedPushHelper,
         private val guardServiceStarter: GuardServiceStarter,
         private val sessionInitializer: SessionInitializer,
         private val applicationContext: Context,
         private val authenticationService: AuthenticationService,
         private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
+        private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) {
 
     private var activeSessionReference: AtomicReference = AtomicReference()
@@ -85,7 +85,7 @@ class ActiveSessionHolder @Inject constructor(
         incomingVerificationRequestHandler.stop()
         pushRuleTriggerListener.stop()
         // No need to unregister the pusher, the sign out will (should?) do it server side.
-        unifiedPushHelper.unregister(pushersManager = null)
+        unregisterUnifiedPushUseCase.execute(pushersManager = null)
         guardServiceStarter.stop()
     }
 
diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
index 64d2a79494..34ba254250 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
@@ -213,7 +213,7 @@ class UnifiedPushHelper @Inject constructor(
                 .show()
     }
 
-    // TODO remove and replace by use case
+    // TODO remove
     suspend fun unregister(pushersManager: PushersManager? = null) {
         val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
         vectorPreferences.setFdroidSyncBackgroundMode(mode)
diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
index d81581679e..71b1a9c033 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
@@ -26,20 +26,19 @@ import javax.inject.Inject
 
 class UnregisterUnifiedPushUseCase @Inject constructor(
         @ApplicationContext private val context: Context,
-        private val pushersManager: PushersManager,
         private val vectorPreferences: VectorPreferences,
         private val unifiedPushStore: UnifiedPushStore,
         private val unifiedPushHelper: UnifiedPushHelper,
 ) {
 
     // TODO add unit tests
-    suspend fun execute() {
+    suspend fun execute(pushersManager: PushersManager?) {
         val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
         vectorPreferences.setFdroidSyncBackgroundMode(mode)
         try {
             unifiedPushHelper.getEndpointOrToken()?.let {
                 Timber.d("Removing $it")
-                pushersManager.unregisterPusher(it)
+                pushersManager?.unregisterPusher(it)
             }
         } catch (e: Exception) {
             Timber.d(e, "Probably unregistering a non existing pusher")
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
index 61c884f0bc..2ce2254f2e 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -18,26 +18,27 @@ package im.vector.app.features.settings.notifications
 
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.PushersManager
-import im.vector.app.core.pushers.UnifiedPushHelper
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
 import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
 import javax.inject.Inject
 
 class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
-        private val unifiedPushHelper: UnifiedPushHelper,
         private val pushersManager: PushersManager,
         private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
         private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+        private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) {
 
+    // TODO update unit tests
     suspend fun execute() {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
         val deviceId = session.sessionParams.deviceId ?: return
         if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
             togglePushNotificationUseCase.execute(deviceId, enabled = false)
         } else {
-            unifiedPushHelper.unregister(pushersManager)
+            unregisterUnifiedPushUseCase.execute(pushersManager)
         }
     }
 }

From 58efe90f7dfa23ffd17110eedec21ef480022fcf Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Tue, 29 Nov 2022 17:10:20 +0100
Subject: [PATCH 455/679] Removing some debug logs

---
 .../vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt | 2 --
 .../java/im/vector/app/features/home/HomeActivityViewModel.kt   | 2 --
 2 files changed, 4 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
index 4a8ff5fb39..e55d0426ba 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
@@ -17,7 +17,6 @@
 package im.vector.app.core.pushers
 
 import im.vector.app.core.di.ActiveSessionHolder
-import timber.log.Timber
 import javax.inject.Inject
 
 class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
@@ -29,7 +28,6 @@ class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
     // TODO add unit tests
     fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
         if (unifiedPushHelper.isEmbeddedDistributor()) {
-            Timber.d("ensureFcmTokenIsRetrieved")
             fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 3fd555bbea..2905decddf 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -131,11 +131,9 @@ class HomeActivityViewModel @AssistedInject constructor(
         viewModelScope.launch {
             when (val result = registerUnifiedPushUseCase.execute(distributor = distributor)) {
                 is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
-                    Timber.d("registerUnifiedPush $distributor need to ask user")
                     _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor(result.distributors))
                 }
                 RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
-                    Timber.d("registerUnifiedPush $distributor success")
                     ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
                 }
             }

From b29191e892bbc9d0b2f15fa106c1524da7c4f180 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Tue, 29 Nov 2022 17:28:48 +0100
Subject: [PATCH 456/679] Using use cases inside component for endpoint testing

---
 .../TestEndpointAsTokenRegistration.kt        | 63 ++++++++++++++-----
 1 file changed, 47 insertions(+), 16 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt
index 3bbff0f2fe..e6cb78d185 100644
--- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt
@@ -17,14 +17,17 @@
 package im.vector.app.features.settings.troubleshoot
 
 import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.Observer
 import androidx.work.WorkInfo
 import androidx.work.WorkManager
 import im.vector.app.R
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.PushersManager
+import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
 import im.vector.app.core.pushers.UnifiedPushHelper
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.session.coroutineScope
+import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.pushers.PusherState
 import javax.inject.Inject
 
@@ -34,6 +37,8 @@ class TestEndpointAsTokenRegistration @Inject constructor(
         private val pushersManager: PushersManager,
         private val activeSessionHolder: ActiveSessionHolder,
         private val unifiedPushHelper: UnifiedPushHelper,
+        private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
+        private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) : TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) {
 
     override fun perform(testParameters: TestParameters) {
@@ -56,27 +61,53 @@ class TestEndpointAsTokenRegistration @Inject constructor(
             )
             quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) {
                 override fun doFix() {
-                    unifiedPushHelper.forceRegister(
-                            context,
-                            pushersManager
-                    )
-                    val workId = pushersManager.enqueueRegisterPusherWithFcmKey(endpoint)
-                    WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo ->
-                        if (workInfo != null) {
-                            if (workInfo.state == WorkInfo.State.SUCCEEDED) {
-                                manager?.retry(testParameters)
-                            } else if (workInfo.state == WorkInfo.State.FAILED) {
-                                manager?.retry(testParameters)
-                            }
-                        }
-                    })
+                    unregisterThenRegister(testParameters, endpoint)
                 }
             }
-
             status = TestStatus.FAILED
         } else {
             description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_success)
             status = TestStatus.SUCCESS
         }
     }
+
+    private fun unregisterThenRegister(testParameters: TestParameters, pushKey: String) {
+        activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch {
+            unregisterUnifiedPushUseCase.execute(pushersManager)
+            registerUnifiedPush(distributor = "", testParameters, pushKey)
+        }
+    }
+
+    private fun registerUnifiedPush(
+            distributor: String,
+            testParameters: TestParameters,
+            pushKey: String,
+    ) {
+        when (val result = registerUnifiedPushUseCase.execute(distributor)) {
+            is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor ->
+                askUserForDistributor(result.distributors, testParameters, pushKey)
+            RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
+                val workId = pushersManager.enqueueRegisterPusherWithFcmKey(pushKey)
+                WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context) { workInfo ->
+                    if (workInfo != null) {
+                        if (workInfo.state == WorkInfo.State.SUCCEEDED) {
+                            manager?.retry(testParameters)
+                        } else if (workInfo.state == WorkInfo.State.FAILED) {
+                            manager?.retry(testParameters)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun askUserForDistributor(
+            distributors: List,
+            testParameters: TestParameters,
+            pushKey: String,
+    ) {
+        unifiedPushHelper.showSelectDistributorDialog(context, distributors) { selection ->
+            registerUnifiedPush(distributor = selection, testParameters, pushKey)
+        }
+    }
 }

From 3f944e9d36c892e88676e3e64fb061afd98b158b Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 11:05:46 +0100
Subject: [PATCH 457/679] Extracting the logic to toggle notifications for
 device into a ViewModel

---
 .../app/core/di/MavericksViewModelModule.kt   |  6 ++
 .../settings/VectorSettingsBaseFragment.kt    | 17 +++-
 ...leNotificationsForCurrentSessionUseCase.kt | 47 +++++------
 ...rSettingsNotificationPreferenceFragment.kt | 73 ++++++++++++++----
 ...ettingsNotificationPreferenceViewAction.kt | 25 ++++++
 ...SettingsNotificationPreferenceViewEvent.kt | 26 +++++++
 ...SettingsNotificationPreferenceViewModel.kt | 77 +++++++++++++++++++
 7 files changed, 226 insertions(+), 45 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt

diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 2242abb7aa..ad3e361775 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -105,6 +105,7 @@ import im.vector.app.features.settings.ignored.IgnoredUsersViewModel
 import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel
 import im.vector.app.features.settings.legals.LegalsViewModel
 import im.vector.app.features.settings.locale.LocalePickerViewModel
+import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceViewModel
 import im.vector.app.features.settings.push.PushGatewaysViewModel
 import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel
 import im.vector.app.features.share.IncomingShareViewModel
@@ -683,4 +684,9 @@ interface MavericksViewModelModule {
     @IntoMap
     @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
     fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+    @Binds
+    @IntoMap
+    @MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class)
+    fun vectorSettingsNotificationPreferenceViewModelFactory(factory: VectorSettingsNotificationPreferenceViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
index 176909b48d..38ba949a49 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
@@ -28,6 +28,8 @@ import im.vector.app.R
 import im.vector.app.core.error.ErrorFormatter
 import im.vector.app.core.extensions.singletonEntryPoint
 import im.vector.app.core.platform.VectorBaseActivity
+import im.vector.app.core.platform.VectorViewEvents
+import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.utils.toast
 import im.vector.app.features.analytics.AnalyticsTracker
 import im.vector.app.features.analytics.plan.MobileScreen
@@ -60,6 +62,19 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
     protected lateinit var session: Session
     protected lateinit var errorFormatter: ErrorFormatter
 
+    /* ==========================================================================================
+     * ViewEvents
+     * ========================================================================================== */
+
+    protected fun  VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
+        viewEvents
+                .stream()
+                .onEach {
+                    observer(it)
+                }
+                .launchIn(viewLifecycleOwner.lifecycleScope)
+    }
+
     /* ==========================================================================================
      * Views
      * ========================================================================================== */
@@ -148,7 +163,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
         }
     }
 
-    protected fun displayErrorDialog(throwable: Throwable) {
+    protected fun displayErrorDialog(throwable: Throwable?) {
         displayErrorDialog(errorFormatter.toHumanReadable(throwable))
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
index 91974787bd..2f8bdd4d0d 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -16,52 +16,45 @@
 
 package im.vector.app.features.settings.notifications
 
-import androidx.fragment.app.FragmentActivity
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
-import im.vector.app.core.pushers.FcmHelper
 import im.vector.app.core.pushers.PushersManager
-import im.vector.app.core.pushers.UnifiedPushHelper
-import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
+import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
 import javax.inject.Inject
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
 
 class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
-        private val unifiedPushHelper: UnifiedPushHelper,
         private val pushersManager: PushersManager,
-        private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
         private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+        private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
         private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
 ) {
 
+    sealed interface EnableNotificationsResult {
+        object Success : EnableNotificationsResult
+        object Failure : EnableNotificationsResult
+        data class NeedToAskUserForDistributor(val distributors: List) : EnableNotificationsResult
+    }
+
     // TODO update unit tests
-    suspend fun execute(fragmentActivity: FragmentActivity) {
+    suspend fun execute(distributor: String = ""): EnableNotificationsResult {
         val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
         if (pusherForCurrentSession == null) {
-            registerPusher(fragmentActivity)
-        }
-
-        val session = activeSessionHolder.getSafeActiveSession() ?: return
-        if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
-            val deviceId = session.sessionParams.deviceId ?: return
-            togglePushNotificationUseCase.execute(deviceId, enabled = true)
-        }
-    }
-
-    private suspend fun registerPusher(fragmentActivity: FragmentActivity) {
-        suspendCoroutine { continuation ->
-            try {
-                unifiedPushHelper.register(fragmentActivity) {
+            when (val result = registerUnifiedPushUseCase.execute(distributor)) {
+                is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
+                    return EnableNotificationsResult.NeedToAskUserForDistributor(result.distributors)
+                }
+                RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
                     ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true)
-                    continuation.resume(Unit)
                 }
-            } catch (error: Exception) {
-                continuation.resumeWithException(error)
             }
         }
+
+        val session = activeSessionHolder.getSafeActiveSession() ?: return EnableNotificationsResult.Failure
+        val deviceId = session.sessionParams.deviceId ?: return EnableNotificationsResult.Failure
+        togglePushNotificationUseCase.execute(deviceId, enabled = true)
+
+        return EnableNotificationsResult.Success
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
index f8c1a9ad44..ae00b3864c 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
@@ -22,6 +22,7 @@ import android.content.Intent
 import android.media.RingtoneManager
 import android.net.Uri
 import android.os.Bundle
+import android.view.View
 import android.widget.Toast
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.distinctUntilChanged
@@ -29,6 +30,7 @@ import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.map
 import androidx.preference.Preference
 import androidx.preference.SwitchPreference
+import com.airbnb.mvrx.fragmentViewModel
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.R
 import im.vector.app.core.di.ActiveSessionHolder
@@ -81,8 +83,6 @@ class VectorSettingsNotificationPreferenceFragment :
     @Inject lateinit var guardServiceStarter: GuardServiceStarter
     @Inject lateinit var vectorFeatures: VectorFeatures
     @Inject lateinit var notificationPermissionManager: NotificationPermissionManager
-    @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase
-    @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase
     @Inject lateinit var ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase
 
     override var titleRes: Int = R.string.settings_notifications
@@ -90,6 +90,8 @@ class VectorSettingsNotificationPreferenceFragment :
 
     private var interactionListener: VectorSettingsFragmentInteractionListener? = null
 
+    private val viewModel: VectorSettingsNotificationPreferenceViewModel by fragmentViewModel()
+
     private val notificationStartForActivityResult = registerStartForActivityResult { _ ->
         // No op
     }
@@ -106,6 +108,22 @@ class VectorSettingsNotificationPreferenceFragment :
         analyticsScreenName = MobileScreen.ScreenName.SettingsNotifications
     }
 
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        observeViewEvents()
+    }
+
+    private fun observeViewEvents() {
+        viewModel.observeViewEvents {
+            when (it) {
+                VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled -> onNotificationsForDeviceEnabled()
+                VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled -> onNotificationsForDeviceDisabled()
+                is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors)
+                VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure -> displayErrorDialog(throwable = null)
+            }
+        }
+    }
+
     override fun bindPref() {
         findPreference(VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY)!!.let { pref ->
             val pushRuleService = session.pushRuleService()
@@ -123,23 +141,15 @@ class VectorSettingsNotificationPreferenceFragment :
         }
 
         findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
-                ?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked ->
-                    if (isChecked) {
-                        enableNotificationsForCurrentSessionUseCase.execute(requireActivity())
-
-                        findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
-                                ?.summary = unifiedPushHelper.getCurrentDistributorName()
-
-                        notificationPermissionManager.eventuallyRequestPermission(
-                                requireActivity(),
-                                postPermissionLauncher,
-                                showRationale = false,
-                                ignorePreference = true
-                        )
+                ?.setOnPreferenceChangeListener { _, isChecked ->
+                    val action = if (isChecked as Boolean) {
+                        VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(pushDistributor = "")
                     } else {
-                        disableNotificationsForCurrentSessionUseCase.execute()
-                        notificationPermissionManager.eventuallyRevokePermission(requireActivity())
+                        VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice
                     }
+                    viewModel.handle(action)
+                    // preference will be updated on ViewEvent reception
+                    false
                 }
 
         findPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)?.let {
@@ -184,6 +194,8 @@ class VectorSettingsNotificationPreferenceFragment :
             if (vectorFeatures.allowExternalUnifiedPushDistributors()) {
                 it.summary = unifiedPushHelper.getCurrentDistributorName()
                 it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
+                    // TODO show dialog to pick a distributor
+                    // TODO call unregister then register only when a new distributor has been selected => use UnifiedPushHelper method
                     unifiedPushHelper.forceRegister(requireActivity(), pushersManager) {
                         ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
                         it.summary = unifiedPushHelper.getCurrentDistributorName()
@@ -203,6 +215,33 @@ class VectorSettingsNotificationPreferenceFragment :
         handleSystemPreference()
     }
 
+    private fun onNotificationsForDeviceEnabled() {
+        findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
+                ?.isChecked = true
+        findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)
+                ?.summary = unifiedPushHelper.getCurrentDistributorName()
+
+        notificationPermissionManager.eventuallyRequestPermission(
+                requireActivity(),
+                postPermissionLauncher,
+                showRationale = false,
+                ignorePreference = true
+        )
+    }
+
+    private fun onNotificationsForDeviceDisabled() {
+        findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
+                ?.isChecked = false
+        notificationPermissionManager.eventuallyRevokePermission(requireActivity())
+    }
+
+    // TODO add an argument to know if unregister should be called
+    private fun askUserToSelectPushDistributor(distributors: List) {
+        unifiedPushHelper.showSelectDistributorDialog(requireContext(), distributors) { selection ->
+            viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection))
+        }
+    }
+
     private fun bindEmailNotifications() {
         val initialEmails = session.getEmailsWithPushInformation()
         bindEmailNotificationCategory(initialEmails)
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt
new file mode 100644
index 0000000000..949dc99993
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.notifications
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed interface VectorSettingsNotificationPreferenceViewAction : VectorViewModelAction {
+    data class EnableNotificationsForDevice(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction
+    object DisableNotificationsForDevice : VectorSettingsNotificationPreferenceViewAction
+    data class RegisterPushDistributor(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
new file mode 100644
index 0000000000..4948ad6e58
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.notifications
+
+import im.vector.app.core.platform.VectorViewEvents
+
+sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents {
+    object NotificationForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent
+    object EnableNotificationForDeviceFailure : VectorSettingsNotificationPreferenceViewEvent
+    object NotificationForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent
+    data class AskUserForPushDistributor(val distributors: List) : VectorSettingsNotificationPreferenceViewEvent
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
new file mode 100644
index 0000000000..0173f4846f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.notifications
+
+import com.airbnb.mvrx.MavericksViewModelFactory
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.VectorDummyViewState
+import im.vector.app.core.platform.VectorViewModel
+import kotlinx.coroutines.launch
+
+class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
+        @Assisted initialState: VectorDummyViewState,
+        private val enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase,
+        private val disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase,
+) : VectorViewModel(initialState) {
+
+    @AssistedFactory
+    interface Factory : MavericksAssistedViewModelFactory {
+        override fun create(initialState: VectorDummyViewState): VectorSettingsNotificationPreferenceViewModel
+    }
+
+    companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+    // TODO add unit tests
+    override fun handle(action: VectorSettingsNotificationPreferenceViewAction) {
+        when (action) {
+            VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice()
+            is VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice -> handleEnableNotificationsForDevice(action.pushDistributor)
+            is VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor -> handleRegisterPushDistributor(action.pushDistributor)
+        }
+    }
+
+    private fun handleDisableNotificationsForDevice() {
+        viewModelScope.launch {
+            disableNotificationsForCurrentSessionUseCase.execute()
+            _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled)
+        }
+    }
+
+    private fun handleEnableNotificationsForDevice(distributor: String) {
+        viewModelScope.launch {
+            when (val result = enableNotificationsForCurrentSessionUseCase.execute(distributor)) {
+                EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure -> {
+                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure)
+                }
+                is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> {
+                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor(result.distributors))
+                }
+                EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success -> {
+                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled)
+                }
+            }
+        }
+    }
+
+    private fun handleRegisterPushDistributor(distributor: String) {
+        handleEnableNotificationsForDevice(distributor)
+    }
+}

From 95556d25515d08b6f9fcc8154be65908110f17ef Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 11:06:24 +0100
Subject: [PATCH 458/679] Change the distributor in dialog cancellation only if
 there is no existing one

---
 .../java/im/vector/app/core/pushers/UnifiedPushHelper.kt   | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
index 34ba254250..87231d1d67 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
@@ -206,8 +206,11 @@ class UnifiedPushHelper @Inject constructor(
                     onDistributorSelected(distributor)
                 }
                 .setOnCancelListener {
-                    // By default, use internal solution (fcm/background sync)
-                    onDistributorSelected(context.packageName)
+                    // we do not want to change the distributor on behalf of the user
+                    if (UnifiedPush.getDistributor(context).isEmpty()) {
+                        // By default, use internal solution (fcm/background sync)
+                        onDistributorSelected(context.packageName)
+                    }
                 }
                 .setCancelable(true)
                 .show()

From 2673979ef8f7feddd60dcd6058a3790435729db0 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 12:04:52 +0100
Subject: [PATCH 459/679] Handling change of notification method

---
 .../pushers/RegisterUnifiedPushUseCase.kt     |  6 ++--
 .../app/core/pushers/UnifiedPushHelper.kt     |  7 ++--
 .../vector/app/features/home/HomeActivity.kt  |  6 ++--
 .../features/home/HomeActivityViewEvents.kt   |  2 +-
 .../features/home/HomeActivityViewModel.kt    |  4 +--
 ...leNotificationsForCurrentSessionUseCase.kt |  6 ++--
 ...rSettingsNotificationPreferenceFragment.kt | 33 ++++++++++---------
 ...SettingsNotificationPreferenceViewEvent.kt |  7 ++--
 ...SettingsNotificationPreferenceViewModel.kt | 31 ++++++++++++++---
 .../TestEndpointAsTokenRegistration.kt        |  7 ++--
 10 files changed, 67 insertions(+), 42 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
index 7aafa07348..58bf0f5050 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
@@ -29,12 +29,12 @@ class RegisterUnifiedPushUseCase @Inject constructor(
 
     sealed interface RegisterUnifiedPushResult {
         object Success : RegisterUnifiedPushResult
-        data class NeedToAskUserForDistributor(val distributors: List) : RegisterUnifiedPushResult
+        object NeedToAskUserForDistributor : RegisterUnifiedPushResult
     }
 
     // TODO add unit tests
     fun execute(distributor: String = ""): RegisterUnifiedPushResult {
-        if(distributor.isNotEmpty()) {
+        if (distributor.isNotEmpty()) {
             saveAndRegisterApp(distributor)
             return RegisterUnifiedPushResult.Success
         }
@@ -55,7 +55,7 @@ class RegisterUnifiedPushUseCase @Inject constructor(
             saveAndRegisterApp(distributors.first())
             RegisterUnifiedPushResult.Success
         } else {
-            RegisterUnifiedPushResult.NeedToAskUserForDistributor(distributors)
+            RegisterUnifiedPushResult.NeedToAskUserForDistributor
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
index 87231d1d67..efa396a980 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
@@ -52,7 +52,7 @@ class UnifiedPushHelper @Inject constructor(
 
     // Called when the home activity starts
     // or when notifications are enabled
-    // TODO remove and replace by use case
+    // TODO remove
     fun register(
             activity: FragmentActivity,
             onDoneRunnable: Runnable? = null,
@@ -70,7 +70,7 @@ class UnifiedPushHelper @Inject constructor(
     // The registration is forced in 2 cases :
     // * in the settings
     // * in the troubleshoot list (doFix)
-    // TODO remove and replace by use case
+    // TODO remove
     fun forceRegister(
             activity: FragmentActivity,
             pushersManager: PushersManager,
@@ -132,6 +132,7 @@ class UnifiedPushHelper @Inject constructor(
         }
     }
 
+    // TODO remove
     // There is no case where this function is called
     // with a saved distributor and/or a pusher
     private fun openDistributorDialogInternal(
@@ -180,7 +181,6 @@ class UnifiedPushHelper @Inject constructor(
     @MainThread
     fun showSelectDistributorDialog(
             context: Context,
-            distributors: List,
             onDistributorSelected: (String) -> Unit,
     ) {
         val internalDistributorName = stringProvider.getString(
@@ -191,6 +191,7 @@ class UnifiedPushHelper @Inject constructor(
                 }
         )
 
+        val distributors = UnifiedPush.getDistributors(context)
         val distributorsName = distributors.map {
             if (it == context.packageName) {
                 internalDistributorName
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
index 14157a1de8..8c6daae95a 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
@@ -266,7 +266,7 @@ class HomeActivity :
                 HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes()
                 HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
                 is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
-                is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors)
+                is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor()
             }
         }
         homeActivityViewModel.onEach { renderState(it) }
@@ -279,8 +279,8 @@ class HomeActivity :
         homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
     }
 
-    private fun askUserToSelectPushDistributor(distributors: List) {
-        unifiedPushHelper.showSelectDistributorDialog(this, distributors) { selection ->
+    private fun askUserToSelectPushDistributor() {
+        unifiedPushHelper.showSelectDistributorDialog(this) { selection ->
             homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection))
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
index 6fdf441d1d..be5aa7def0 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt
@@ -39,5 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
     data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
     object StartRecoverySetupFlow : HomeActivityViewEvents
     data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents
-    data class AskUserForPushDistributor(val distributors: List) : HomeActivityViewEvents
+    object AskUserForPushDistributor : HomeActivityViewEvents
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 2905decddf..7ffc46218c 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -129,9 +129,9 @@ class HomeActivityViewModel @AssistedInject constructor(
 
     private fun registerUnifiedPush(distributor: String) {
         viewModelScope.launch {
-            when (val result = registerUnifiedPushUseCase.execute(distributor = distributor)) {
+            when (registerUnifiedPushUseCase.execute(distributor = distributor)) {
                 is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
-                    _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor(result.distributors))
+                    _viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor)
                 }
                 RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
                     ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
index 2f8bdd4d0d..e0b0a872f8 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -34,16 +34,16 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
     sealed interface EnableNotificationsResult {
         object Success : EnableNotificationsResult
         object Failure : EnableNotificationsResult
-        data class NeedToAskUserForDistributor(val distributors: List) : EnableNotificationsResult
+        object NeedToAskUserForDistributor : EnableNotificationsResult
     }
 
     // TODO update unit tests
     suspend fun execute(distributor: String = ""): EnableNotificationsResult {
         val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
         if (pusherForCurrentSession == null) {
-            when (val result = registerUnifiedPushUseCase.execute(distributor)) {
+            when (registerUnifiedPushUseCase.execute(distributor)) {
                 is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
-                    return EnableNotificationsResult.NeedToAskUserForDistributor(result.distributors)
+                    return EnableNotificationsResult.NeedToAskUserForDistributor
                 }
                 RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
                     ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true)
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
index ae00b3864c..238ed4218c 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
@@ -116,10 +116,11 @@ class VectorSettingsNotificationPreferenceFragment :
     private fun observeViewEvents() {
         viewModel.observeViewEvents {
             when (it) {
-                VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled -> onNotificationsForDeviceEnabled()
-                VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled -> onNotificationsForDeviceDisabled()
-                is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor(it.distributors)
+                VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled -> onNotificationsForDeviceEnabled()
+                VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled -> onNotificationsForDeviceDisabled()
+                is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor()
                 VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure -> displayErrorDialog(throwable = null)
+                VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged -> onNotificationMethodChanged()
             }
         }
     }
@@ -194,14 +195,7 @@ class VectorSettingsNotificationPreferenceFragment :
             if (vectorFeatures.allowExternalUnifiedPushDistributors()) {
                 it.summary = unifiedPushHelper.getCurrentDistributorName()
                 it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
-                    // TODO show dialog to pick a distributor
-                    // TODO call unregister then register only when a new distributor has been selected => use UnifiedPushHelper method
-                    unifiedPushHelper.forceRegister(requireActivity(), pushersManager) {
-                        ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
-                        it.summary = unifiedPushHelper.getCurrentDistributorName()
-                        session.pushersService().refreshPushers()
-                        refreshBackgroundSyncPrefs()
-                    }
+                    askUserToSelectPushDistributor(withUnregister = true)
                     true
                 }
             } else {
@@ -235,13 +229,22 @@ class VectorSettingsNotificationPreferenceFragment :
         notificationPermissionManager.eventuallyRevokePermission(requireActivity())
     }
 
-    // TODO add an argument to know if unregister should be called
-    private fun askUserToSelectPushDistributor(distributors: List) {
-        unifiedPushHelper.showSelectDistributorDialog(requireContext(), distributors) { selection ->
-            viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection))
+    private fun askUserToSelectPushDistributor(withUnregister: Boolean = false) {
+        unifiedPushHelper.showSelectDistributorDialog(requireContext()) { selection ->
+            if (withUnregister) {
+                viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection))
+            } else {
+                viewModel.handle(VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(selection))
+            }
         }
     }
 
+    private fun onNotificationMethodChanged() {
+        findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)?.summary = unifiedPushHelper.getCurrentDistributorName()
+        session.pushersService().refreshPushers()
+        refreshBackgroundSyncPrefs()
+    }
+
     private fun bindEmailNotifications() {
         val initialEmails = session.getEmailsWithPushInformation()
         bindEmailNotificationCategory(initialEmails)
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
index 4948ad6e58..e4cf8e1973 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
@@ -19,8 +19,9 @@ package im.vector.app.features.settings.notifications
 import im.vector.app.core.platform.VectorViewEvents
 
 sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents {
-    object NotificationForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent
+    object NotificationsForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent
     object EnableNotificationForDeviceFailure : VectorSettingsNotificationPreferenceViewEvent
-    object NotificationForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent
-    data class AskUserForPushDistributor(val distributors: List) : VectorSettingsNotificationPreferenceViewEvent
+    object NotificationsForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent
+    object AskUserForPushDistributor : VectorSettingsNotificationPreferenceViewEvent
+    object NotificationMethodChanged : VectorSettingsNotificationPreferenceViewEvent
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
index 0173f4846f..59c26749c9 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
@@ -24,12 +24,22 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
 import im.vector.app.core.di.hiltMavericksViewModelFactory
 import im.vector.app.core.platform.VectorDummyViewState
 import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
+import im.vector.app.core.pushers.PushersManager
+import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
+import im.vector.app.features.settings.VectorPreferences
 import kotlinx.coroutines.launch
 
 class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
         @Assisted initialState: VectorDummyViewState,
+        private val pushersManager: PushersManager,
+        private val vectorPreferences: VectorPreferences,
         private val enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase,
         private val disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase,
+        private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
+        private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
+        private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
 ) : VectorViewModel(initialState) {
 
     @AssistedFactory
@@ -51,27 +61,38 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
     private fun handleDisableNotificationsForDevice() {
         viewModelScope.launch {
             disableNotificationsForCurrentSessionUseCase.execute()
-            _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceDisabled)
+            _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled)
         }
     }
 
     private fun handleEnableNotificationsForDevice(distributor: String) {
         viewModelScope.launch {
-            when (val result = enableNotificationsForCurrentSessionUseCase.execute(distributor)) {
+            when (enableNotificationsForCurrentSessionUseCase.execute(distributor)) {
                 EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure -> {
                     _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure)
                 }
                 is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> {
-                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor(result.distributors))
+                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor)
                 }
                 EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success -> {
-                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationForDeviceEnabled)
+                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled)
                 }
             }
         }
     }
 
     private fun handleRegisterPushDistributor(distributor: String) {
-        handleEnableNotificationsForDevice(distributor)
+        viewModelScope.launch {
+            unregisterUnifiedPushUseCase.execute(pushersManager)
+            when (registerUnifiedPushUseCase.execute(distributor)) {
+                RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
+                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor)
+                }
+                RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
+                    ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
+                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged)
+                }
+            }
+        }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt
index e6cb78d185..b355b55903 100644
--- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt
@@ -83,9 +83,9 @@ class TestEndpointAsTokenRegistration @Inject constructor(
             testParameters: TestParameters,
             pushKey: String,
     ) {
-        when (val result = registerUnifiedPushUseCase.execute(distributor)) {
+        when (registerUnifiedPushUseCase.execute(distributor)) {
             is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor ->
-                askUserForDistributor(result.distributors, testParameters, pushKey)
+                askUserForDistributor(testParameters, pushKey)
             RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
                 val workId = pushersManager.enqueueRegisterPusherWithFcmKey(pushKey)
                 WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context) { workInfo ->
@@ -102,11 +102,10 @@ class TestEndpointAsTokenRegistration @Inject constructor(
     }
 
     private fun askUserForDistributor(
-            distributors: List,
             testParameters: TestParameters,
             pushKey: String,
     ) {
-        unifiedPushHelper.showSelectDistributorDialog(context, distributors) { selection ->
+        unifiedPushHelper.showSelectDistributorDialog(context) { selection ->
             registerUnifiedPush(distributor = selection, testParameters, pushKey)
         }
     }

From 740ed8963892ff76a4482c43c090996ca7579f76 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 14:27:18 +0100
Subject: [PATCH 460/679] Removing the old methods from helper

---
 .../app/core/pushers/UnifiedPushHelper.kt     | 155 ------------------
 1 file changed, 155 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
index efa396a980..9f96f13ee7 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt
@@ -18,20 +18,12 @@ package im.vector.app.core.pushers
 
 import android.content.Context
 import androidx.annotation.MainThread
-import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.lifecycleScope
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import im.vector.app.R
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.core.utils.getApplicationLabel
-import im.vector.app.features.VectorFeatures
-import im.vector.app.features.settings.BackgroundSyncMode
-import im.vector.app.features.settings.VectorPreferences
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.Matrix
 import org.matrix.android.sdk.api.cache.CacheStrategy
 import org.matrix.android.sdk.api.util.MatrixJsonParser
@@ -44,140 +36,10 @@ class UnifiedPushHelper @Inject constructor(
         private val context: Context,
         private val unifiedPushStore: UnifiedPushStore,
         private val stringProvider: StringProvider,
-        private val vectorPreferences: VectorPreferences,
         private val matrix: Matrix,
-        private val vectorFeatures: VectorFeatures,
         private val fcmHelper: FcmHelper,
 ) {
 
-    // Called when the home activity starts
-    // or when notifications are enabled
-    // TODO remove
-    fun register(
-            activity: FragmentActivity,
-            onDoneRunnable: Runnable? = null,
-    ) {
-        registerInternal(
-                activity,
-                onDoneRunnable = onDoneRunnable
-        )
-    }
-
-    // If registration is forced:
-    // * the current distributor (if any) is removed
-    // * The dialog is opened
-    //
-    // The registration is forced in 2 cases :
-    // * in the settings
-    // * in the troubleshoot list (doFix)
-    // TODO remove
-    fun forceRegister(
-            activity: FragmentActivity,
-            pushersManager: PushersManager,
-            @MainThread onDoneRunnable: Runnable? = null
-    ) {
-        registerInternal(
-                activity,
-                force = true,
-                pushersManager = pushersManager,
-                onDoneRunnable = onDoneRunnable
-        )
-    }
-
-    // TODO remove
-    private fun registerInternal(
-            activity: FragmentActivity,
-            force: Boolean = false,
-            pushersManager: PushersManager? = null,
-            onDoneRunnable: Runnable? = null
-    ) {
-        activity.lifecycleScope.launch(Dispatchers.IO) {
-            Timber.d("registerInternal force=$force, $activity on thread ${Thread.currentThread()}")
-            if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
-                UnifiedPush.saveDistributor(context, context.packageName)
-                UnifiedPush.registerApp(context)
-                withContext(Dispatchers.Main) {
-                    onDoneRunnable?.run()
-                }
-                return@launch
-            }
-            if (force) {
-                // Un-register first
-                unregister(pushersManager)
-            }
-            // the !force should not be needed
-            if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) {
-                UnifiedPush.registerApp(context)
-                withContext(Dispatchers.Main) {
-                    onDoneRunnable?.run()
-                }
-                return@launch
-            }
-
-            val distributors = UnifiedPush.getDistributors(context)
-
-            if (!force && distributors.size == 1) {
-                UnifiedPush.saveDistributor(context, distributors.first())
-                UnifiedPush.registerApp(context)
-                withContext(Dispatchers.Main) {
-                    onDoneRunnable?.run()
-                }
-            } else {
-                openDistributorDialogInternal(
-                        activity = activity,
-                        onDoneRunnable = onDoneRunnable,
-                        distributors = distributors
-                )
-            }
-        }
-    }
-
-    // TODO remove
-    // There is no case where this function is called
-    // with a saved distributor and/or a pusher
-    private fun openDistributorDialogInternal(
-            activity: FragmentActivity,
-            onDoneRunnable: Runnable?,
-            distributors: List
-    ) {
-        val internalDistributorName = stringProvider.getString(
-                if (fcmHelper.isFirebaseAvailable()) {
-                    R.string.unifiedpush_distributor_fcm_fallback
-                } else {
-                    R.string.unifiedpush_distributor_background_sync
-                }
-        )
-
-        val distributorsName = distributors.map {
-            if (it == context.packageName) {
-                internalDistributorName
-            } else {
-                context.getApplicationLabel(it)
-            }
-        }
-
-        MaterialAlertDialogBuilder(activity)
-                .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title))
-                .setItems(distributorsName.toTypedArray()) { _, which ->
-                    val distributor = distributors[which]
-
-                    activity.lifecycleScope.launch {
-                        UnifiedPush.saveDistributor(context, distributor)
-                        Timber.i("Saving distributor: $distributor")
-                        UnifiedPush.registerApp(context)
-                        onDoneRunnable?.run()
-                    }
-                }
-                .setOnCancelListener {
-                    // By default, use internal solution (fcm/background sync)
-                    UnifiedPush.saveDistributor(context, context.packageName)
-                    UnifiedPush.registerApp(context)
-                    onDoneRunnable?.run()
-                }
-                .setCancelable(true)
-                .show()
-    }
-
     @MainThread
     fun showSelectDistributorDialog(
             context: Context,
@@ -217,23 +79,6 @@ class UnifiedPushHelper @Inject constructor(
                 .show()
     }
 
-    // TODO remove
-    suspend fun unregister(pushersManager: PushersManager? = null) {
-        val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
-        vectorPreferences.setFdroidSyncBackgroundMode(mode)
-        try {
-            getEndpointOrToken()?.let {
-                Timber.d("Removing $it")
-                pushersManager?.unregisterPusher(it)
-            }
-        } catch (e: Exception) {
-            Timber.d(e, "Probably unregistering a non existing pusher")
-        }
-        unifiedPushStore.storeUpEndpoint(null)
-        unifiedPushStore.storePushGateway(null)
-        UnifiedPush.unregisterApp(context)
-    }
-
     @JsonClass(generateAdapter = true)
     internal data class DiscoveryResponse(
             @Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()

From aa3a808d2cc670301b5d1920d532b30b85be5b48 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 14:42:56 +0100
Subject: [PATCH 461/679] Do not ask to select push distributor in home if
 notifications are disabled

---
 .../im/vector/app/features/home/HomeActivityViewModel.kt  | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 7ffc46218c..cf4bce12f0 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -116,7 +116,7 @@ class HomeActivityViewModel @AssistedInject constructor(
     private fun initialize() {
         if (isInitialized) return
         isInitialized = true
-        registerUnifiedPush(distributor = "")
+        registerUnifiedPushIfNeeded()
         cleanupFiles()
         observeInitialSync()
         checkSessionPushIsOn()
@@ -127,6 +127,12 @@ class HomeActivityViewModel @AssistedInject constructor(
         viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
     }
 
+    private fun registerUnifiedPushIfNeeded() {
+        if(vectorPreferences.areNotificationEnabledForDevice()) {
+            registerUnifiedPush(distributor = "")
+        }
+    }
+
     private fun registerUnifiedPush(distributor: String) {
         viewModelScope.launch {
             when (registerUnifiedPushUseCase.execute(distributor = distributor)) {

From a3815d70128e35639b2b5b020d96db8927306267 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 15:12:07 +0100
Subject: [PATCH 462/679] Update unit tests

---
 ...leNotificationsForCurrentSessionUseCase.kt |  1 -
 ...leNotificationsForCurrentSessionUseCase.kt |  1 -
 ...tificationsForCurrentSessionUseCaseTest.kt | 12 +--
 ...tificationsForCurrentSessionUseCaseTest.kt | 74 ++++++++++++-------
 .../im/vector/app/test/fakes/FakeFcmHelper.kt | 11 +--
 .../app/test/fakes/FakeUnifiedPushHelper.kt   | 23 ------
 6 files changed, 56 insertions(+), 66 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
index 2ce2254f2e..84d92c4291 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -31,7 +31,6 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) {
 
-    // TODO update unit tests
     suspend fun execute() {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
         val deviceId = session.sessionParams.deviceId ?: return
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
index e0b0a872f8..99fb249384 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -37,7 +37,6 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
         object NeedToAskUserForDistributor : EnableNotificationsResult
     }
 
-    // TODO update unit tests
     suspend fun execute(distributor: String = ""): EnableNotificationsResult {
         val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
         if (pusherForCurrentSession == null) {
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
index e460413a39..386e68ee3a 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -16,11 +16,11 @@
 
 package im.vector.app.features.settings.notifications
 
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
 import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
-import im.vector.app.test.fakes.FakeUnifiedPushHelper
 import io.mockk.coJustRun
 import io.mockk.coVerify
 import io.mockk.every
@@ -33,17 +33,17 @@ private const val A_SESSION_ID = "session-id"
 class DisableNotificationsForCurrentSessionUseCaseTest {
 
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
-    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
     private val fakePushersManager = FakePushersManager()
     private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk()
     private val fakeTogglePushNotificationUseCase = mockk()
+    private val fakeUnregisterUnifiedPushUseCase = mockk()
 
     private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
             activeSessionHolder = fakeActiveSessionHolder.instance,
-            unifiedPushHelper = fakeUnifiedPushHelper.instance,
             pushersManager = fakePushersManager.instance,
             checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
             togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
+            unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
     )
 
     @Test
@@ -67,12 +67,14 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
         val fakeSession = fakeActiveSessionHolder.fakeSession
         fakeSession.givenSessionId(A_SESSION_ID)
         every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
-        fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance)
+        coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
 
         // When
         disableNotificationsForCurrentSessionUseCase.execute()
 
         // Then
-        fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance)
+        coVerify {
+            fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
+        }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
index eb6629cb13..c923f0c7d6 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
@@ -16,18 +16,18 @@
 
 package im.vector.app.features.settings.notifications
 
-import androidx.fragment.app.FragmentActivity
-import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
+import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
+import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
-import im.vector.app.test.fakes.FakeFcmHelper
 import im.vector.app.test.fakes.FakePushersManager
-import im.vector.app.test.fakes.FakeUnifiedPushHelper
 import io.mockk.coJustRun
-import io.mockk.coVerify
 import io.mockk.every
+import io.mockk.justRun
 import io.mockk.mockk
+import io.mockk.verify
 import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBe
 import org.junit.Test
 
 private const val A_SESSION_ID = "session-id"
@@ -35,53 +35,71 @@ private const val A_SESSION_ID = "session-id"
 class EnableNotificationsForCurrentSessionUseCaseTest {
 
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
-    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
     private val fakePushersManager = FakePushersManager()
-    private val fakeFcmHelper = FakeFcmHelper()
-    private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk()
     private val fakeTogglePushNotificationUseCase = mockk()
+    private val fakeRegisterUnifiedPushUseCase = mockk()
+    private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk()
 
     private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase(
             activeSessionHolder = fakeActiveSessionHolder.instance,
-            unifiedPushHelper = fakeUnifiedPushHelper.instance,
             pushersManager = fakePushersManager.instance,
-            fcmHelper = fakeFcmHelper.instance,
-            checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
             togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
+            registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
+            ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
     )
 
     @Test
-    fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest {
+    fun `given no existing pusher and a registered distributor when execute then a new pusher is registered and result is success`() = runTest {
         // Given
-        val fragmentActivity = mockk()
+        val aDistributor = "distributor"
+        val fakeSession = fakeActiveSessionHolder.fakeSession
+        fakeSession.givenSessionId(A_SESSION_ID)
         fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
-        fakeUnifiedPushHelper.givenRegister(fragmentActivity)
-        fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
-        fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance)
-        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false
+        every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
+        justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
+        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
 
         // When
-        enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
+        val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
 
         // Then
-        fakeUnifiedPushHelper.verifyRegister(fragmentActivity)
-        fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true)
+        result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success
+        verify {
+            fakeRegisterUnifiedPushUseCase.execute(aDistributor)
+            fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = true)
+        }
     }
 
     @Test
-    fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest {
+    fun `given no existing pusher and a no registered distributor when execute then result is need to ask user for distributor`() = runTest {
         // Given
-        val fragmentActivity = mockk()
-        fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk())
+        val aDistributor = "distributor"
+        fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
+        every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
+
+        // When
+        val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
+
+        // Then
+        result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor
+        verify {
+            fakeRegisterUnifiedPushUseCase.execute(aDistributor)
+        }
+    }
+
+    @Test
+    fun `given no deviceId for current session when execute then result is failure`() = runTest {
+        // Given
+        val aDistributor = "distributor"
         val fakeSession = fakeActiveSessionHolder.fakeSession
-        fakeSession.givenSessionId(A_SESSION_ID)
-        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true
-        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
+        fakeSession.givenSessionId(null)
+        fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk())
+        every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
 
         // When
-        enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity)
+        val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
 
         // Then
-        coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) }
+        result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure
     }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt
index 11abf18794..07eef36dc1 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt
@@ -16,7 +16,6 @@
 
 package im.vector.app.test.fakes
 
-import androidx.fragment.app.FragmentActivity
 import im.vector.app.core.pushers.FcmHelper
 import im.vector.app.core.pushers.PushersManager
 import io.mockk.justRun
@@ -27,18 +26,14 @@ class FakeFcmHelper {
 
     val instance = mockk()
 
-    fun givenEnsureFcmTokenIsRetrieved(
-            fragmentActivity: FragmentActivity,
-            pushersManager: PushersManager,
-    ) {
-        justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) }
+    fun givenEnsureFcmTokenIsRetrieved(pushersManager: PushersManager) {
+        justRun { instance.ensureFcmTokenIsRetrieved(pushersManager, any()) }
     }
 
     fun verifyEnsureFcmTokenIsRetrieved(
-            fragmentActivity: FragmentActivity,
             pushersManager: PushersManager,
             registerPusher: Boolean,
     ) {
-        verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) }
+        verify { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
index 1f2cc8a1ce..5bc57ead07 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
@@ -16,37 +16,14 @@
 
 package im.vector.app.test.fakes
 
-import androidx.fragment.app.FragmentActivity
-import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.UnifiedPushHelper
-import io.mockk.coJustRun
-import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.mockk
-import io.mockk.verify
 
 class FakeUnifiedPushHelper {
 
     val instance = mockk()
 
-    fun givenRegister(fragmentActivity: FragmentActivity) {
-        every { instance.register(fragmentActivity, any()) } answers {
-            secondArg().run()
-        }
-    }
-
-    fun verifyRegister(fragmentActivity: FragmentActivity) {
-        verify { instance.register(fragmentActivity, any()) }
-    }
-
-    fun givenUnregister(pushersManager: PushersManager) {
-        coJustRun { instance.unregister(pushersManager) }
-    }
-
-    fun verifyUnregister(pushersManager: PushersManager) {
-        coVerify { instance.unregister(pushersManager) }
-    }
-
     fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) {
         every { instance.isEmbeddedDistributor() } returns isEmbedded
     }

From 46ccf4d73fea7909b64999f7d0786e2684b33e4c Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 16:02:55 +0100
Subject: [PATCH 463/679] Adding unit tests for register and unregister use
 cases

---
 .../pushers/RegisterUnifiedPushUseCase.kt     |   1 -
 .../pushers/UnregisterUnifiedPushUseCase.kt   |   1 -
 .../pushers/RegisterUnifiedPushUseCaseTest.kt | 158 ++++++++++++++++++
 .../UnregisterUnifiedPushUseCaseTest.kt       |  83 +++++++++
 .../im/vector/app/test/fakes/FakeContext.kt   |   4 +
 .../app/test/fakes/FakePushersManager.kt      |  10 ++
 .../app/test/fakes/FakeUnifiedPushHelper.kt   |   4 +
 .../app/test/fakes/FakeUnifiedPushStore.kt    |  43 +++++
 .../app/test/fakes/FakeVectorFeatures.kt      |   4 +
 .../app/test/fakes/FakeVectorPreferences.kt   |   9 +
 10 files changed, 315 insertions(+), 2 deletions(-)
 create mode 100644 vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt
 create mode 100644 vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt
 create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt

diff --git a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
index 58bf0f5050..aa3652a54f 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt
@@ -32,7 +32,6 @@ class RegisterUnifiedPushUseCase @Inject constructor(
         object NeedToAskUserForDistributor : RegisterUnifiedPushResult
     }
 
-    // TODO add unit tests
     fun execute(distributor: String = ""): RegisterUnifiedPushResult {
         if (distributor.isNotEmpty()) {
             saveAndRegisterApp(distributor)
diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
index 71b1a9c033..acad3e649f 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt
@@ -31,7 +31,6 @@ class UnregisterUnifiedPushUseCase @Inject constructor(
         private val unifiedPushHelper: UnifiedPushHelper,
 ) {
 
-    // TODO add unit tests
     suspend fun execute(pushersManager: PushersManager?) {
         val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
         vectorPreferences.setFdroidSyncBackgroundMode(mode)
diff --git a/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt
new file mode 100644
index 0000000000..c72c519172
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.pushers
+
+import im.vector.app.test.fakes.FakeContext
+import im.vector.app.test.fakes.FakeVectorFeatures
+import io.mockk.every
+import io.mockk.justRun
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import io.mockk.verify
+import io.mockk.verifyAll
+import io.mockk.verifyOrder
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBe
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.unifiedpush.android.connector.UnifiedPush
+
+class RegisterUnifiedPushUseCaseTest {
+
+    private val fakeContext = FakeContext()
+    private val fakeVectorFeatures = FakeVectorFeatures()
+
+    private val registerUnifiedPushUseCase = RegisterUnifiedPushUseCase(
+            context = fakeContext.instance,
+            vectorFeatures = fakeVectorFeatures,
+    )
+
+    @Before
+    fun setup() {
+        mockkStatic(UnifiedPush::class)
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given non empty distributor when execute then distributor is saved and app is registered`() = runTest {
+        // Given
+        val aDistributor = "distributor"
+        justRun { UnifiedPush.registerApp(any()) }
+        justRun { UnifiedPush.saveDistributor(any(), any()) }
+
+        // When
+        val result = registerUnifiedPushUseCase.execute(aDistributor)
+
+        // Then
+        result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
+        verifyOrder {
+            UnifiedPush.saveDistributor(fakeContext.instance, aDistributor)
+            UnifiedPush.registerApp(fakeContext.instance)
+        }
+    }
+
+    @Test
+    fun `given external distributors are not allowed when execute then internal distributor is saved and app is registered`() = runTest {
+        // Given
+        val aPackageName = "packageName"
+        fakeContext.givenPackageName(aPackageName)
+        justRun { UnifiedPush.registerApp(any()) }
+        justRun { UnifiedPush.saveDistributor(any(), any()) }
+        fakeVectorFeatures.givenExternalDistributorsAreAllowed(false)
+
+        // When
+        val result = registerUnifiedPushUseCase.execute()
+
+        // Then
+        result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
+        verifyOrder {
+            UnifiedPush.saveDistributor(fakeContext.instance, aPackageName)
+            UnifiedPush.registerApp(fakeContext.instance)
+        }
+    }
+
+    @Test
+    fun `given a saved distributor and external distributors are allowed when execute then app is registered`() = runTest {
+        // Given
+        justRun { UnifiedPush.registerApp(any()) }
+        val aDistributor = "distributor"
+        every { UnifiedPush.getDistributor(any()) } returns aDistributor
+        fakeVectorFeatures.givenExternalDistributorsAreAllowed(true)
+
+        // When
+        val result = registerUnifiedPushUseCase.execute()
+
+        // Then
+        result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
+        verifyAll {
+            UnifiedPush.getDistributor(fakeContext.instance)
+            UnifiedPush.registerApp(fakeContext.instance)
+        }
+    }
+
+    @Test
+    fun `given no saved distributor and a unique distributor available when execute then the distributor is saved and app is registered`() = runTest {
+        // Given
+        justRun { UnifiedPush.registerApp(any()) }
+        justRun { UnifiedPush.saveDistributor(any(), any()) }
+        every { UnifiedPush.getDistributor(any()) } returns ""
+        fakeVectorFeatures.givenExternalDistributorsAreAllowed(true)
+        val aDistributor = "distributor"
+        every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor)
+
+        // When
+        val result = registerUnifiedPushUseCase.execute()
+
+        // Then
+        result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
+        verifyOrder {
+            UnifiedPush.getDistributor(fakeContext.instance)
+            UnifiedPush.getDistributors(fakeContext.instance)
+            UnifiedPush.saveDistributor(fakeContext.instance, aDistributor)
+            UnifiedPush.registerApp(fakeContext.instance)
+        }
+    }
+
+    @Test
+    fun `given no saved distributor and multiple distributors available when execute then result is to ask user`() = runTest {
+        // Given
+        every { UnifiedPush.getDistributor(any()) } returns ""
+        fakeVectorFeatures.givenExternalDistributorsAreAllowed(true)
+        val aDistributor1 = "distributor1"
+        val aDistributor2 = "distributor2"
+        every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor1, aDistributor2)
+
+        // When
+        val result = registerUnifiedPushUseCase.execute()
+
+        // Then
+        result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
+        verifyOrder {
+            UnifiedPush.getDistributor(fakeContext.instance)
+            UnifiedPush.getDistributors(fakeContext.instance)
+        }
+        verify(inverse = true) {
+            UnifiedPush.saveDistributor(any(), any())
+            UnifiedPush.registerApp(any())
+        }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt
new file mode 100644
index 0000000000..bee545b3e1
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.pushers
+
+import im.vector.app.features.settings.BackgroundSyncMode
+import im.vector.app.test.fakes.FakeContext
+import im.vector.app.test.fakes.FakePushersManager
+import im.vector.app.test.fakes.FakeUnifiedPushHelper
+import im.vector.app.test.fakes.FakeUnifiedPushStore
+import im.vector.app.test.fakes.FakeVectorPreferences
+import io.mockk.justRun
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import io.mockk.verifyAll
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.unifiedpush.android.connector.UnifiedPush
+
+class UnregisterUnifiedPushUseCaseTest {
+
+    private val fakeContext = FakeContext()
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeUnifiedPushStore = FakeUnifiedPushStore()
+    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
+
+    private val unregisterUnifiedPushUseCase = UnregisterUnifiedPushUseCase(
+            context = fakeContext.instance,
+            vectorPreferences = fakeVectorPreferences.instance,
+            unifiedPushStore = fakeUnifiedPushStore.instance,
+            unifiedPushHelper = fakeUnifiedPushHelper.instance,
+    )
+
+    @Before
+    fun setup() {
+        mockkStatic(UnifiedPush::class)
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given pushersManager when execute then unregister and clean everything which is needed`() = runTest {
+        // Given
+        val aEndpoint = "endpoint"
+        fakeUnifiedPushHelper.givenGetEndpointOrTokenReturns(aEndpoint)
+        val aPushersManager = FakePushersManager()
+        aPushersManager.givenUnregisterPusher(aEndpoint)
+        justRun { UnifiedPush.unregisterApp(any()) }
+        fakeVectorPreferences.givenSetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME)
+        fakeUnifiedPushStore.givenStorePushGateway(null)
+        fakeUnifiedPushStore.givenStoreUpEndpoint(null)
+
+        // When
+        unregisterUnifiedPushUseCase.execute(aPushersManager.instance)
+
+        // Then
+        fakeVectorPreferences.verifySetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME)
+        aPushersManager.verifyUnregisterPusher(aEndpoint)
+        verifyAll {
+            UnifiedPush.unregisterApp(fakeContext.instance)
+        }
+        fakeUnifiedPushStore.verifyStorePushGateway(null)
+        fakeUnifiedPushStore.verifyStoreUpEndpoint(null)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
index 9a94313fec..f8c568e908 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
@@ -81,4 +81,8 @@ class FakeContext(
         givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance)
         return fakeClipboardManager
     }
+
+    fun givenPackageName(name: String) {
+        every { instance.packageName } returns name
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt
index 46d852f4f8..3dd3854a18 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt
@@ -17,6 +17,8 @@
 package im.vector.app.test.fakes
 
 import im.vector.app.core.pushers.PushersManager
+import io.mockk.coJustRun
+import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.mockk
 import org.matrix.android.sdk.api.session.pushers.Pusher
@@ -28,4 +30,12 @@ class FakePushersManager {
     fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) {
         every { instance.getPusherForCurrentSession() } returns pusher
     }
+
+    fun givenUnregisterPusher(pushKey: String) {
+        coJustRun { instance.unregisterPusher(pushKey) }
+    }
+
+    fun verifyUnregisterPusher(pushKey: String) {
+        coVerify { instance.unregisterPusher(pushKey) }
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
index 5bc57ead07..99b5b75874 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
@@ -27,4 +27,8 @@ class FakeUnifiedPushHelper {
     fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) {
         every { instance.isEmbeddedDistributor() } returns isEmbedded
     }
+
+    fun givenGetEndpointOrTokenReturns(endpoint: String?) {
+        every { instance.getEndpointOrToken() } returns endpoint
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt
new file mode 100644
index 0000000000..9b09bec688
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fakes
+
+import im.vector.app.core.pushers.UnifiedPushStore
+import io.mockk.justRun
+import io.mockk.mockk
+import io.mockk.verify
+
+class FakeUnifiedPushStore {
+
+    val instance = mockk()
+
+    fun givenStoreUpEndpoint(endpoint: String?) {
+        justRun { instance.storeUpEndpoint(endpoint) }
+    }
+
+    fun verifyStoreUpEndpoint(endpoint: String?) {
+        verify { instance.storeUpEndpoint(endpoint) }
+    }
+
+    fun givenStorePushGateway(gateway: String?) {
+        justRun { instance.storePushGateway(gateway) }
+    }
+
+    fun verifyStorePushGateway(gateway: String?) {
+        verify { instance.storePushGateway(gateway) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
index c3c2fa684f..b399f0baa4 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt
@@ -54,4 +54,8 @@ class FakeVectorFeatures : VectorFeatures by spyk() {
     fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) {
         every { isUnverifiedSessionsAlertEnabled() } returns isEnabled
     }
+
+    fun givenExternalDistributorsAreAllowed(allowed: Boolean) {
+        every { allowExternalUnifiedPushDistributors() } returns allowed
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 77df3ffc7a..7970c14e90 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.test.fakes
 
+import im.vector.app.features.settings.BackgroundSyncMode
 import im.vector.app.features.settings.VectorPreferences
 import io.mockk.every
 import io.mockk.justRun
@@ -60,4 +61,12 @@ class FakeVectorPreferences {
     fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) {
         every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis
     }
+
+    fun givenSetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) {
+        justRun { instance.setFdroidSyncBackgroundMode(mode) }
+    }
+
+    fun verifySetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) {
+        verify { instance.setFdroidSyncBackgroundMode(mode) }
+    }
 }

From 2a8c72bdcf60409b78e51fad4af53c49bcacc5d4 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 16:05:25 +0100
Subject: [PATCH 464/679] Fixing code style issues

---
 .../java/im/vector/app/core/di/MavericksViewModelModule.kt    | 4 +++-
 vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt  | 1 -
 .../java/im/vector/app/features/home/HomeActivityViewModel.kt | 2 +-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index ad3e361775..b58d584dad 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -688,5 +688,7 @@ interface MavericksViewModelModule {
     @Binds
     @IntoMap
     @MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class)
-    fun vectorSettingsNotificationPreferenceViewModelFactory(factory: VectorSettingsNotificationPreferenceViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+    fun vectorSettingsNotificationPreferenceViewModelFactory(
+            factory: VectorSettingsNotificationPreferenceViewModel.Factory
+    ): MavericksAssistedViewModelFactory<*, *>
 }
diff --git a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt
index 0cc251ce31..381348638d 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt
@@ -16,7 +16,6 @@
 
 package im.vector.app.core.pushers
 
-import android.app.Activity
 import im.vector.app.core.di.ActiveSessionHolder
 
 interface FcmHelper {
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index cf4bce12f0..26034fc09c 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -128,7 +128,7 @@ class HomeActivityViewModel @AssistedInject constructor(
     }
 
     private fun registerUnifiedPushIfNeeded() {
-        if(vectorPreferences.areNotificationEnabledForDevice()) {
+        if (vectorPreferences.areNotificationEnabledForDevice()) {
             registerUnifiedPush(distributor = "")
         }
     }

From e78e19285344a9b68db8542226dcacac56c110de Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 16:37:15 +0100
Subject: [PATCH 465/679] Adding unit tests for FCM token retrieval

---
 .../EnsureFcmTokenIsRetrievedUseCase.kt       |   1 -
 .../EnsureFcmTokenIsRetrievedUseCaseTest.kt   | 106 ++++++++++++++++++
 .../im/vector/app/test/fakes/FakeFcmHelper.kt |   3 +-
 3 files changed, 108 insertions(+), 2 deletions(-)
 create mode 100644 vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt

diff --git a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
index e55d0426ba..cb955e01f7 100644
--- a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt
@@ -25,7 +25,6 @@ class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
 ) {
 
-    // TODO add unit tests
     fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
         if (unifiedPushHelper.isEmbeddedDistributor()) {
             fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
diff --git a/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt
new file mode 100644
index 0000000000..03a43a5b55
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.pushers
+
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeFcmHelper
+import im.vector.app.test.fakes.FakePushersManager
+import im.vector.app.test.fakes.FakeUnifiedPushHelper
+import im.vector.app.test.fixtures.PusherFixture
+import io.mockk.verify
+import org.junit.Test
+
+class EnsureFcmTokenIsRetrievedUseCaseTest {
+
+    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
+    private val fakeFcmHelper = FakeFcmHelper()
+    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+
+    private val ensureFcmTokenIsRetrievedUseCase = EnsureFcmTokenIsRetrievedUseCase(
+            unifiedPushHelper = fakeUnifiedPushHelper.instance,
+            fcmHelper = fakeFcmHelper.instance,
+            activeSessionHolder = fakeActiveSessionHolder.instance,
+    )
+
+    @Test
+    fun `given no registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved with register pusher option`() {
+        // Given
+        val aPushersManager = FakePushersManager()
+        fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
+        fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance)
+        val aSessionId = "aSessionId"
+        fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId)
+        val expectedPusher = PusherFixture.aPusher(deviceId = "")
+        fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher))
+
+        // When
+        ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true)
+
+        // Then
+        fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true)
+    }
+
+    @Test
+    fun `given a registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() {
+        // Given
+        val aPushersManager = FakePushersManager()
+        fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
+        fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance)
+        val aSessionId = "aSessionId"
+        fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId)
+        val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId)
+        fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher))
+
+        // When
+        ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true)
+
+        // Then
+        fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false)
+    }
+
+    @Test
+    fun `given no registering asked and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() {
+        // Given
+        val aPushersManager = FakePushersManager()
+        fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true)
+        fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance)
+        val aSessionId = "aSessionId"
+        fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId)
+        val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId)
+        fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher))
+
+        // When
+        ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = false)
+
+        // Then
+        fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false)
+    }
+
+    @Test
+    fun `given distributor as not embedded when execute then nothing is done`() {
+        // Given
+        val aPushersManager = FakePushersManager()
+        fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(false)
+
+        // When
+        ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true)
+
+        // Then
+        fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true, inverse = true)
+        fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false, inverse = true)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt
index 07eef36dc1..4c210215ec 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt
@@ -33,7 +33,8 @@ class FakeFcmHelper {
     fun verifyEnsureFcmTokenIsRetrieved(
             pushersManager: PushersManager,
             registerPusher: Boolean,
+            inverse: Boolean = false,
     ) {
-        verify { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) }
+        verify(inverse = inverse) { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) }
     }
 }

From d31652e91007e67f6ea5a8065c6f032af9607e6f Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 30 Nov 2022 17:21:48 +0100
Subject: [PATCH 466/679] Adding unit tests for settings ViewModel

---
 ...SettingsNotificationPreferenceViewModel.kt |   1 -
 ...ingsNotificationPreferenceViewModelTest.kt | 202 ++++++++++++++++++
 .../app/test/fakes/FakeVectorPreferences.kt   |   4 +
 3 files changed, 206 insertions(+), 1 deletion(-)
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt

diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
index 59c26749c9..d6a9c621f2 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
@@ -49,7 +49,6 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
 
     companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
 
-    // TODO add unit tests
     override fun handle(action: VectorSettingsNotificationPreferenceViewAction) {
         when (action) {
             VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice()
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
new file mode 100644
index 0000000000..f9d7527316
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.notifications
+
+import com.airbnb.mvrx.test.MavericksTestRule
+import im.vector.app.core.platform.VectorDummyViewState
+import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
+import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
+import im.vector.app.test.fakes.FakePushersManager
+import im.vector.app.test.fakes.FakeVectorPreferences
+import im.vector.app.test.test
+import im.vector.app.test.testDispatcher
+import io.mockk.coEvery
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.coVerifyOrder
+import io.mockk.justRun
+import io.mockk.mockk
+import org.junit.Rule
+import org.junit.Test
+
+class VectorSettingsNotificationPreferenceViewModelTest {
+
+    @get:Rule
+    val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
+
+    private val fakePushersManager = FakePushersManager()
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeEnableNotificationsForCurrentSessionUseCase = mockk()
+    private val fakeDisableNotificationsForCurrentSessionUseCase = mockk()
+    private val fakeUnregisterUnifiedPushUseCase = mockk()
+    private val fakeRegisterUnifiedPushUseCase = mockk()
+    private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk()
+
+    private fun createViewModel() = VectorSettingsNotificationPreferenceViewModel(
+            initialState = VectorDummyViewState(),
+            pushersManager = fakePushersManager.instance,
+            vectorPreferences = fakeVectorPreferences.instance,
+            enableNotificationsForCurrentSessionUseCase = fakeEnableNotificationsForCurrentSessionUseCase,
+            disableNotificationsForCurrentSessionUseCase = fakeDisableNotificationsForCurrentSessionUseCase,
+            unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
+            registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
+            ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
+    )
+
+    @Test
+    fun `given DisableNotificationsForDevice action when handling action then disable use case is called`() {
+        // Given
+        val viewModel = createViewModel()
+        val action = VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice
+        coJustRun { fakeDisableNotificationsForCurrentSessionUseCase.execute() }
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.handle(action)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+        coVerify {
+            fakeDisableNotificationsForCurrentSessionUseCase.execute()
+        }
+    }
+
+    @Test
+    fun `given EnableNotificationsForDevice action and enable success when handling action then enable use case is called`() {
+        // Given
+        val viewModel = createViewModel()
+        val aDistributor = "aDistributor"
+        val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor)
+        coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns
+                EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.handle(action)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+        coVerify {
+            fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor)
+        }
+    }
+
+    @Test
+    fun `given EnableNotificationsForDevice action and enable needs user choice when handling action then enable use case is called`() {
+        // Given
+        val viewModel = createViewModel()
+        val aDistributor = "aDistributor"
+        val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor)
+        coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns
+                EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.handle(action)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+        coVerify {
+            fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor)
+        }
+    }
+
+    @Test
+    fun `given EnableNotificationsForDevice action and enable failure when handling action then enable use case is called`() {
+        // Given
+        val viewModel = createViewModel()
+        val aDistributor = "aDistributor"
+        val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor)
+        coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns
+                EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.handle(action)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+        coVerify {
+            fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor)
+        }
+    }
+
+    @Test
+    fun `given RegisterPushDistributor action and register success when handling action then register use case is called`() {
+        // Given
+        val viewModel = createViewModel()
+        val aDistributor = "aDistributor"
+        val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor)
+        coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
+        coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
+        val areNotificationsEnabled = true
+        fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled)
+        justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.handle(action)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+        coVerifyOrder {
+            fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
+            fakeRegisterUnifiedPushUseCase.execute(aDistributor)
+            fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = areNotificationsEnabled)
+        }
+    }
+
+    @Test
+    fun `given RegisterPushDistributor action and register needs user choice when handling action then register use case is called`() {
+        // Given
+        val viewModel = createViewModel()
+        val aDistributor = "aDistributor"
+        val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor)
+        coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
+        coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.handle(action)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+        coVerifyOrder {
+            fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
+            fakeRegisterUnifiedPushUseCase.execute(aDistributor)
+        }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 7970c14e90..06efca1bf7 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -69,4 +69,8 @@ class FakeVectorPreferences {
     fun verifySetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) {
         verify { instance.setFdroidSyncBackgroundMode(mode) }
     }
+
+    fun givenAreNotificationsEnabledForDevice(notificationsEnabled: Boolean) {
+        every { instance.areNotificationEnabledForDevice() } returns notificationsEnabled
+    }
 }

From f8c59f6b0c82608484463d1f3157c1203102bccc Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 1 Dec 2022 10:22:09 +0100
Subject: [PATCH 467/679] Removing unused import

---
 .../app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt     | 1 -
 1 file changed, 1 deletion(-)

diff --git a/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt
index 03a43a5b55..fca49adc9b 100644
--- a/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt
@@ -21,7 +21,6 @@ import im.vector.app.test.fakes.FakeFcmHelper
 import im.vector.app.test.fakes.FakePushersManager
 import im.vector.app.test.fakes.FakeUnifiedPushHelper
 import im.vector.app.test.fixtures.PusherFixture
-import io.mockk.verify
 import org.junit.Test
 
 class EnsureFcmTokenIsRetrievedUseCaseTest {

From 0c6781e9ef1ca7784539f645a8ce7b2b508c918e Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 16 Nov 2022 14:37:48 +0100
Subject: [PATCH 468/679] Adding changelog entry

---
 changelog.d/7596.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7596.feature

diff --git a/changelog.d/7596.feature b/changelog.d/7596.feature
new file mode 100644
index 0000000000..022d86342b
--- /dev/null
+++ b/changelog.d/7596.feature
@@ -0,0 +1 @@
+Save m.local_notification_settings. event in account_data

From 9d684bc021b5af66b8309d24dd398f61bdbad7f6 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 16 Nov 2022 16:31:00 +0100
Subject: [PATCH 469/679] Check if account data has content to decide if push
 notifications can be toggled using account data

---
 ...ePushNotificationsViaAccountDataUseCase.kt |  7 +++++-
 ...hNotificationsViaAccountDataUseCaseTest.kt | 23 +++++++++++++++++--
 2 files changed, 27 insertions(+), 3 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt
index 194a2aebbf..a1b87bc396 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt
@@ -16,8 +16,10 @@
 
 package im.vector.app.features.settings.devices.v2.notification
 
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toModel
 import javax.inject.Inject
 
 class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() {
@@ -25,6 +27,9 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor
     fun execute(session: Session, deviceId: String): Boolean {
         return session
                 .accountDataService()
-                .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
+                .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
+                ?.content
+                .toModel()
+                ?.isSilenced != null
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt
index 37433364e8..94f142cbe6 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt
@@ -20,7 +20,9 @@ import im.vector.app.test.fakes.FakeSession
 import io.mockk.mockk
 import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toContent
 
 private const val A_DEVICE_ID = "device-id"
 
@@ -32,13 +34,13 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
             CheckIfCanTogglePushNotificationsViaAccountDataUseCase()
 
     @Test
-    fun `given current session and an account data for the device id when execute then result is true`() {
+    fun `given current session and an account data with a content for the device id when execute then result is true`() {
         // Given
         fakeSession
                 .accountDataService()
                 .givenGetUserAccountDataEventReturns(
                         type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
-                        content = mockk(),
+                        content = LocalNotificationSettingsContent(isSilenced = true).toContent(),
                 )
 
         // When
@@ -48,6 +50,23 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
         result shouldBeEqualTo true
     }
 
+    @Test
+    fun `given current session and an account data with empty content for the device id when execute then result is false`() {
+        // Given
+        fakeSession
+                .accountDataService()
+                .givenGetUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
+                        content = mockk(),
+                )
+
+        // When
+        val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+
+        // Then
+        result shouldBeEqualTo false
+    }
+
     @Test
     fun `given current session and NO account data for the device id when execute then result is false`() {
         // Given

From c56eb331db5cfe652d70ba4d3f293dbb118dd55c Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 16 Nov 2022 16:38:46 +0100
Subject: [PATCH 470/679] Update use cases to enable/disable push notifications
 for the current session

---
 .../DisableNotificationsForCurrentSessionUseCase.kt          | 5 ++---
 .../DisableNotificationsForCurrentSessionUseCaseTest.kt      | 2 ++
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
index 84d92c4291..4d890ca678 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -34,9 +34,8 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
     suspend fun execute() {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
         val deviceId = session.sessionParams.deviceId ?: return
-        if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
-            togglePushNotificationUseCase.execute(deviceId, enabled = false)
-        } else {
+        togglePushNotificationUseCase.execute(deviceId, enabled = false)
+        if (!checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
             unregisterUnifiedPushUseCase.execute(pushersManager)
         }
     }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
index 386e68ee3a..a84aa4b055 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -67,6 +67,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
         val fakeSession = fakeActiveSessionHolder.fakeSession
         fakeSession.givenSessionId(A_SESSION_ID)
         every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
+        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
         coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
 
         // When
@@ -74,6 +75,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
 
         // Then
         coVerify {
+            fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false)
             fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
         }
     }

From 81c64503f2f5956c3ef331128b251538c16c9b22 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 16 Nov 2022 18:02:59 +0100
Subject: [PATCH 471/679] Adding SetNotificationSettingsAccountDataUseCase

---
 ...tNotificationSettingsAccountDataUseCase.kt | 36 +++++++++++++
 ...ificationSettingsAccountDataUseCaseTest.kt | 51 +++++++++++++++++++
 2 files changed, 87 insertions(+)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt
new file mode 100644
index 0000000000..f0ec9d5ddc
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.core.di.ActiveSessionHolder
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toContent
+import javax.inject.Inject
+
+class SetNotificationSettingsAccountDataUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+) {
+
+    suspend fun execute(deviceId: String, localNotificationSettingsContent: LocalNotificationSettingsContent) {
+        val session = activeSessionHolder.getSafeActiveSession() ?: return
+        session.accountDataService().updateUserAccountData(
+                UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId,
+                localNotificationSettingsContent.toContent(),
+        )
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
new file mode 100644
index 0000000000..8f72f0946f
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toContent
+
+class SetNotificationSettingsAccountDataUseCaseTest {
+
+    private val activeSessionHolder = FakeActiveSessionHolder()
+
+    private val setNotificationSettingsAccountDataUseCase = SetNotificationSettingsAccountDataUseCase(
+            activeSessionHolder = activeSessionHolder.instance,
+    )
+
+    @Test
+    fun `given a content when execute then update local notification settings with this content`() = runTest {
+        // Given
+        val sessionId = "a_session_id"
+        val localNotificationSettingsContent = LocalNotificationSettingsContent()
+        val fakeSession = activeSessionHolder.fakeSession
+        fakeSession.accountDataService().givenUpdateUserAccountDataEventSucceeds()
+
+        // When
+        setNotificationSettingsAccountDataUseCase.execute(sessionId, localNotificationSettingsContent)
+
+        // Then
+        activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds(
+                UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
+                localNotificationSettingsContent.toContent(),
+        )
+    }
+}

From b163b42d3d54f7c296d723b491b2e7d067f71ff5 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 17 Nov 2022 10:14:51 +0100
Subject: [PATCH 472/679] Use new sub usecase in the
 TogglePushNotificationUseCase

---
 .../TogglePushNotificationUseCase.kt          |  9 +++----
 .../TogglePushNotificationUseCaseTest.kt      | 27 +++++++++----------
 2 files changed, 15 insertions(+), 21 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
index 7969bbbe9b..b8a6d7343b 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
@@ -18,14 +18,14 @@ package im.vector.app.features.settings.devices.v2.notification
 
 import im.vector.app.core.di.ActiveSessionHolder
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
-import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
-import org.matrix.android.sdk.api.session.events.model.toContent
 import javax.inject.Inject
 
+// TODO rename into ToggleNotificationsUseCase
 class TogglePushNotificationUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
         private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
         private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+        private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
 ) {
 
     suspend fun execute(deviceId: String, enabled: Boolean) {
@@ -40,10 +40,7 @@ class TogglePushNotificationUseCase @Inject constructor(
 
         if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) {
             val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
-            session.accountDataService().updateUserAccountData(
-                    UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId,
-                    newNotificationSettingsContent.toContent(),
-            )
+            setNotificationSettingsAccountDataUseCase.execute(deviceId, newNotificationSettingsContent)
         }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
index 35c5979e53..e443dc3e94 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
@@ -18,13 +18,13 @@ package im.vector.app.features.settings.devices.v2.notification
 
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fixtures.PusherFixture
+import io.mockk.coJustRun
+import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.mockk
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
-import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
-import org.matrix.android.sdk.api.session.events.model.toContent
 
 class TogglePushNotificationUseCaseTest {
 
@@ -33,12 +33,15 @@ class TogglePushNotificationUseCaseTest {
             mockk()
     private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
             mockk()
+    private val fakeSetNotificationSettingsAccountDataUseCase =
+            mockk()
 
     private val togglePushNotificationUseCase =
             TogglePushNotificationUseCase(
                     activeSessionHolder = activeSessionHolder.instance,
                     checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
                     checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+                    setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
             )
 
     @Test
@@ -66,26 +69,20 @@ class TogglePushNotificationUseCaseTest {
     fun `when execute, then toggle local notification settings`() = runTest {
         // Given
         val sessionId = "a_session_id"
-        val pushers = listOf(
-                PusherFixture.aPusher(deviceId = sessionId, enabled = false),
-                PusherFixture.aPusher(deviceId = "another id", enabled = false)
-        )
         val fakeSession = activeSessionHolder.fakeSession
-        fakeSession.pushersService().givenPushersLive(pushers)
-        fakeSession.accountDataService().givenGetUserAccountDataEventReturns(
-                UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
-                LocalNotificationSettingsContent(isSilenced = true).toContent()
-        )
         every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
         every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) }
+        val expectedLocalNotificationSettingsContent = LocalNotificationSettingsContent(
+                isSilenced = false
+        )
 
         // When
         togglePushNotificationUseCase.execute(sessionId, true)
 
         // Then
-        activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds(
-                UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
-                LocalNotificationSettingsContent(isSilenced = false).toContent(),
-        )
+        coVerify {
+            fakeSetNotificationSettingsAccountDataUseCase.execute(sessionId, expectedLocalNotificationSettingsContent)
+        }
     }
 }

From 14b21dc0397c5b4e77a5ad5c94e220847b82cb14 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 17 Nov 2022 10:34:11 +0100
Subject: [PATCH 473/679] Adding use cases to create and delete notifications
 settings in account data

---
 .../LocalNotificationSettingsContent.kt       |  3 +-
 ...eNotificationSettingsAccountDataUseCase.kt | 38 ++++++++++++
 ...eNotificationSettingsAccountDataUseCase.kt | 33 +++++++++++
 ...ificationSettingsAccountDataUseCaseTest.kt | 59 +++++++++++++++++++
 ...ificationSettingsAccountDataUseCaseTest.kt | 49 +++++++++++++++
 5 files changed, 181 insertions(+), 1 deletion(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
index 2a95ccce7a..6998d9dcf2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
@@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class LocalNotificationSettingsContent(
-        @Json(name = "is_silenced") val isSilenced: Boolean = false
+        @Json(name = "is_silenced")
+        val isSilenced: Boolean? = false
 )
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt
new file mode 100644
index 0000000000..e2ee19e5cd
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.features.settings.VectorPreferences
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+
+class CreateNotificationSettingsAccountDataUseCase @Inject constructor(
+        private val vectorPreferences: VectorPreferences,
+        private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase
+) {
+
+    // TODO to be called on session start when background sync is enabled + when switching to background sync
+    suspend fun execute(session: Session) {
+        val deviceId = session.sessionParams.deviceId ?: return
+        val isSilenced = !vectorPreferences.areNotificationEnabledForDevice()
+        val notificationSettingsContent = LocalNotificationSettingsContent(
+                isSilenced = isSilenced
+        )
+        setNotificationSettingsAccountDataUseCase.execute(deviceId, notificationSettingsContent)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
new file mode 100644
index 0000000000..51c24e500c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import javax.inject.Inject
+
+class DeleteNotificationSettingsAccountDataUseCase @Inject constructor(
+        private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
+) {
+
+    // TODO to be called when switching to push notifications method
+    suspend fun execute(deviceId: String) {
+        val emptyNotificationSettingsContent = LocalNotificationSettingsContent(
+                isSilenced = null
+        )
+        setNotificationSettingsAccountDataUseCase.execute(deviceId, emptyNotificationSettingsContent)
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt
new file mode 100644
index 0000000000..e4cadaa005
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeVectorPreferences
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+
+class CreateNotificationSettingsAccountDataUseCaseTest {
+
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
+
+    private val createNotificationSettingsAccountDataUseCase = CreateNotificationSettingsAccountDataUseCase(
+        vectorPreferences = fakeVectorPreferences.instance,
+        setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
+    )
+
+    @Test
+    fun `given a device id when execute then content with the current notification preference is set for the account data`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val session = FakeSession()
+        session.givenSessionId(aDeviceId)
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) }
+        val areNotificationsEnabled = true
+        fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
+        val expectedContent = LocalNotificationSettingsContent(
+                isSilenced = !areNotificationsEnabled
+        )
+
+        // When
+        createNotificationSettingsAccountDataUseCase.execute(session)
+
+        // Then
+        verify { fakeVectorPreferences.instance.areNotificationEnabledForDevice() }
+        coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
new file mode 100644
index 0000000000..038a1f436a
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+
+class DeleteNotificationSettingsAccountDataUseCaseTest {
+
+    private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
+
+    private val deleteNotificationSettingsAccountDataUseCase = DeleteNotificationSettingsAccountDataUseCase(
+            setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
+    )
+
+    @Test
+    fun `given a device id when execute then empty content is set for the account data`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) }
+        val expectedContent = LocalNotificationSettingsContent(
+                isSilenced = null
+        )
+
+        // When
+        deleteNotificationSettingsAccountDataUseCase.execute(aDeviceId)
+
+        // Then
+        coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) }
+    }
+}

From 7c51174d7ef3940cbb550dcbd89dcce942ae4bec Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 17 Nov 2022 11:04:21 +0100
Subject: [PATCH 474/679] Renaming some use cases to be consistent

---
 ...CanToggleNotificationsViaPusherUseCase.kt} |  2 +-
 ...ggleNotificationsViaAccountDataUseCase.kt} |  2 +-
 ...CanToggleNotificationsViaPusherUseCase.kt} |  2 +-
 .../GetNotificationsStatusUseCase.kt          |  8 ++---
 ...seCase.kt => ToggleNotificationUseCase.kt} | 11 ++++---
 .../v2/overview/SessionOverviewViewModel.kt   |  6 ++--
 ...leNotificationsForCurrentSessionUseCase.kt | 12 ++++----
 ...leNotificationsForCurrentSessionUseCase.kt |  6 ++--
 ...oggleNotificationsViaPusherUseCaseTest.kt} |  8 ++---
 ...NotificationsViaAccountDataUseCaseTest.kt} | 12 ++++----
 ...oggleNotificationsViaPusherUseCaseTest.kt} |  8 ++---
 .../GetNotificationsStatusUseCaseTest.kt      | 28 ++++++++---------
 ...st.kt => ToggleNotificationUseCaseTest.kt} | 30 +++++++++----------
 ...tificationsForCurrentSessionUseCaseTest.kt | 22 +++++++-------
 ...tificationsForCurrentSessionUseCaseTest.kt |  8 ++---
 .../FakeTogglePushNotificationUseCase.kt      |  4 +--
 16 files changed, 85 insertions(+), 84 deletions(-)
 rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CanTogglePushNotificationsViaPusherUseCase.kt => CanToggleNotificationsViaPusherUseCase.kt} (94%)
 rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt => CheckIfCanToggleNotificationsViaAccountDataUseCase.kt} (93%)
 rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaPusherUseCase.kt => CheckIfCanToggleNotificationsViaPusherUseCase.kt} (92%)
 rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{TogglePushNotificationUseCase.kt => ToggleNotificationUseCase.kt} (74%)
 rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{CanTogglePushNotificationsViaPusherUseCaseTest.kt => CanToggleNotificationsViaPusherUseCaseTest.kt} (87%)
 rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt => CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt} (83%)
 rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt => CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt} (82%)
 rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{TogglePushNotificationUseCaseTest.kt => ToggleNotificationUseCaseTest.kt} (70%)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt
similarity index 94%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt
index 0125d92ba6..96521ec78c 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt
@@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.flow.unwrap
 import javax.inject.Inject
 
-class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() {
+class CanToggleNotificationsViaPusherUseCase @Inject constructor() {
 
     fun execute(session: Session): Flow {
         return session
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt
similarity index 93%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt
index a1b87bc396..b006e3da45 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.events.model.toModel
 import javax.inject.Inject
 
-class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() {
+class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor() {
 
     fun execute(session: Session, deviceId: String): Boolean {
         return session
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt
similarity index 92%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt
index ca314bf145..1dc186be7c 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt
@@ -19,7 +19,7 @@ package im.vector.app.features.settings.devices.v2.notification
 import org.matrix.android.sdk.api.session.Session
 import javax.inject.Inject
 
-class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() {
+class CheckIfCanToggleNotificationsViaPusherUseCase @Inject constructor() {
 
     fun execute(session: Session): Boolean {
         return session
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
index 03e4e31f2e..f98fd63efb 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
@@ -30,13 +30,13 @@ import org.matrix.android.sdk.flow.unwrap
 import javax.inject.Inject
 
 class GetNotificationsStatusUseCase @Inject constructor(
-        private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase,
-        private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+        private val canToggleNotificationsViaPusherUseCase: CanToggleNotificationsViaPusherUseCase,
+        private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase,
 ) {
 
     fun execute(session: Session, deviceId: String): Flow {
         return when {
-            checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> {
+            checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId) -> {
                 session.flow()
                         .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
                         .unwrap()
@@ -44,7 +44,7 @@ class GetNotificationsStatusUseCase @Inject constructor(
                         .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
                         .distinctUntilChanged()
             }
-            else -> canTogglePushNotificationsViaPusherUseCase.execute(session)
+            else -> canToggleNotificationsViaPusherUseCase.execute(session)
                     .flatMapLatest { canToggle ->
                         if (canToggle) {
                             session.flow()
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
similarity index 74%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
index b8a6d7343b..d0e1ea2a7a 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
@@ -20,25 +20,24 @@ import im.vector.app.core.di.ActiveSessionHolder
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import javax.inject.Inject
 
-// TODO rename into ToggleNotificationsUseCase
-class TogglePushNotificationUseCase @Inject constructor(
+class ToggleNotificationUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
-        private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
-        private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+        private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase,
+        private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase,
         private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
 ) {
 
     suspend fun execute(deviceId: String, enabled: Boolean) {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
 
-        if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
+        if (checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) {
             val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
             devicePusher?.let { pusher ->
                 session.pushersService().togglePusher(pusher, enabled)
             }
         }
 
-        if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) {
+        if (checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId)) {
             val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
             setNotificationSettingsAccountDataUseCase.execute(deviceId, newNotificationSettingsContent)
         }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
index 472e0a4269..0ddf688514 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
@@ -31,7 +31,7 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
 import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase
 import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
 import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
-import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
@@ -54,7 +54,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
         private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
         private val pendingAuthHandler: PendingAuthHandler,
         private val activeSessionHolder: ActiveSessionHolder,
-        private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+        private val toggleNotificationUseCase: ToggleNotificationUseCase,
         private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase,
         refreshDevicesUseCase: RefreshDevicesUseCase,
         private val vectorPreferences: VectorPreferences,
@@ -228,7 +228,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
 
     private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) {
         viewModelScope.launch {
-            togglePushNotificationUseCase.execute(action.deviceId, action.enabled)
+            toggleNotificationUseCase.execute(action.deviceId, action.enabled)
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
index 4d890ca678..380c3b5e4e 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -19,23 +19,23 @@ package im.vector.app.features.settings.notifications
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
-import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import javax.inject.Inject
 
 class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
         private val pushersManager: PushersManager,
-        private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase,
-        private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+        private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase,
+        private val toggleNotificationUseCase: ToggleNotificationUseCase,
         private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) {
 
     suspend fun execute() {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
         val deviceId = session.sessionParams.deviceId ?: return
-        togglePushNotificationUseCase.execute(deviceId, enabled = false)
-        if (!checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) {
+        toggleNotificationUseCase.execute(deviceId, enabled = false)
+        if (!checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) {
             unregisterUnifiedPushUseCase.execute(pushersManager)
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
index 99fb249384..89633a10c2 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -20,13 +20,13 @@ import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import javax.inject.Inject
 
 class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
         private val pushersManager: PushersManager,
-        private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
+        private val toggleNotificationUseCase: ToggleNotificationUseCase,
         private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
         private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
 ) {
@@ -52,7 +52,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
 
         val session = activeSessionHolder.getSafeActiveSession() ?: return EnableNotificationsResult.Failure
         val deviceId = session.sessionParams.deviceId ?: return EnableNotificationsResult.Failure
-        togglePushNotificationUseCase.execute(deviceId, enabled = true)
+        toggleNotificationUseCase.execute(deviceId, enabled = true)
 
         return EnableNotificationsResult.Success
     }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt
similarity index 87%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt
index 997fa827f5..3284adb32d 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt
@@ -30,13 +30,13 @@ import org.junit.Test
 
 private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true)
 
-class CanTogglePushNotificationsViaPusherUseCaseTest {
+class CanToggleNotificationsViaPusherUseCaseTest {
 
     private val fakeSession = FakeSession()
     private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
 
-    private val canTogglePushNotificationsViaPusherUseCase =
-            CanTogglePushNotificationsViaPusherUseCase()
+    private val canToggleNotificationsViaPusherUseCase =
+            CanToggleNotificationsViaPusherUseCase()
 
     @Before
     fun setUp() {
@@ -57,7 +57,7 @@ class CanTogglePushNotificationsViaPusherUseCaseTest {
                 .givenAsFlow()
 
         // When
-        val result = canTogglePushNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull()
+        val result = canToggleNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull()
 
         // Then
         result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt
similarity index 83%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt
index 94f142cbe6..de225d36ac 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt
@@ -26,12 +26,12 @@ import org.matrix.android.sdk.api.session.events.model.toContent
 
 private const val A_DEVICE_ID = "device-id"
 
-class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
+class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest {
 
     private val fakeSession = FakeSession()
 
-    private val checkIfCanTogglePushNotificationsViaAccountDataUseCase =
-            CheckIfCanTogglePushNotificationsViaAccountDataUseCase()
+    private val checkIfCanToggleNotificationsViaAccountDataUseCase =
+            CheckIfCanToggleNotificationsViaAccountDataUseCase()
 
     @Test
     fun `given current session and an account data with a content for the device id when execute then result is true`() {
@@ -44,7 +44,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
                 )
 
         // When
-        val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+        val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
 
         // Then
         result shouldBeEqualTo true
@@ -61,7 +61,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
                 )
 
         // When
-        val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+        val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
 
         // Then
         result shouldBeEqualTo false
@@ -78,7 +78,7 @@ class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest {
                 )
 
         // When
-        val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+        val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
 
         // Then
         result shouldBeEqualTo false
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt
similarity index 82%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt
index 508a05acd6..64beb7b8e2 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt
@@ -23,12 +23,12 @@ import org.junit.Test
 
 private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true)
 
-class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest {
+class CheckIfCanToggleNotificationsViaPusherUseCaseTest {
 
     private val fakeSession = FakeSession()
 
-    private val checkIfCanTogglePushNotificationsViaPusherUseCase =
-            CheckIfCanTogglePushNotificationsViaPusherUseCase()
+    private val checkIfCanToggleNotificationsViaPusherUseCase =
+            CheckIfCanToggleNotificationsViaPusherUseCase()
 
     @Test
     fun `given current session when execute then toggle capability is returned`() {
@@ -38,7 +38,7 @@ class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest {
                 .givenCapabilities(A_HOMESERVER_CAPABILITIES)
 
         // When
-        val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
+        val result = checkIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession)
 
         // Then
         result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
index b38367b098..7c0b0a8693 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
@@ -46,15 +46,15 @@ class GetNotificationsStatusUseCaseTest {
     val instantTaskExecutorRule = InstantTaskExecutorRule()
 
     private val fakeSession = FakeSession()
-    private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
-            mockk()
-    private val fakeCanTogglePushNotificationsViaPusherUseCase =
-            mockk()
+    private val fakeCheckIfCanToggleNotificationsViaAccountDataUseCase =
+            mockk()
+    private val fakeCanToggleNotificationsViaPusherUseCase =
+            mockk()
 
     private val getNotificationsStatusUseCase =
             GetNotificationsStatusUseCase(
-                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
-                    canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase,
+                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
+                    canTogglePushNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase,
             )
 
     @Before
@@ -70,8 +70,8 @@ class GetNotificationsStatusUseCaseTest {
     @Test
     fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
         // Given
-        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
-        every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
+        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
 
         // When
         val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
@@ -80,8 +80,8 @@ class GetNotificationsStatusUseCaseTest {
         result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
         verifyOrder {
             // we should first check account data
-            fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
-            fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession)
+            fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+            fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession)
         }
     }
 
@@ -95,8 +95,8 @@ class GetNotificationsStatusUseCaseTest {
                 )
         )
         fakeSession.pushersService().givenPushersLive(pushers)
-        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
-        every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
+        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
 
         // When
         val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
@@ -116,8 +116,8 @@ class GetNotificationsStatusUseCaseTest {
                                 isSilenced = false
                         ).toContent(),
                 )
-        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true
-        every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
+        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true
+        every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
 
         // When
         val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
similarity index 70%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
index e443dc3e94..99be7c7ea9 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
@@ -26,21 +26,21 @@ import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 
-class TogglePushNotificationUseCaseTest {
+class ToggleNotificationUseCaseTest {
 
     private val activeSessionHolder = FakeActiveSessionHolder()
-    private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase =
-            mockk()
-    private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
-            mockk()
+    private val fakeCheckIfCanToggleNotificationsViaPusherUseCase =
+            mockk()
+    private val fakeCheckIfCanToggleNotificationsViaAccountDataUseCase =
+            mockk()
     private val fakeSetNotificationSettingsAccountDataUseCase =
             mockk()
 
-    private val togglePushNotificationUseCase =
-            TogglePushNotificationUseCase(
+    private val toggleNotificationUseCase =
+            ToggleNotificationUseCase(
                     activeSessionHolder = activeSessionHolder.instance,
-                    checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
-                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase,
+                    checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
+                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
                     setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
             )
 
@@ -55,11 +55,11 @@ class TogglePushNotificationUseCaseTest {
         val fakeSession = activeSessionHolder.fakeSession
         fakeSession.pushersService().givenPushersLive(pushers)
         fakeSession.pushersService().givenGetPushers(pushers)
-        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true
-        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false
+        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true
+        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false
 
         // When
-        togglePushNotificationUseCase.execute(sessionId, true)
+        toggleNotificationUseCase.execute(sessionId, true)
 
         // Then
         activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true)
@@ -70,15 +70,15 @@ class TogglePushNotificationUseCaseTest {
         // Given
         val sessionId = "a_session_id"
         val fakeSession = activeSessionHolder.fakeSession
-        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
-        every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true
+        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false
+        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) }
         val expectedLocalNotificationSettingsContent = LocalNotificationSettingsContent(
                 isSilenced = false
         )
 
         // When
-        togglePushNotificationUseCase.execute(sessionId, true)
+        toggleNotificationUseCase.execute(sessionId, true)
 
         // Then
         coVerify {
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
index a84aa4b055..153b79f1a8 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -16,6 +16,8 @@
 
 package im.vector.app.features.settings.notifications
 
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
 import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
@@ -34,15 +36,15 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
 
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakePushersManager = FakePushersManager()
-    private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk()
-    private val fakeTogglePushNotificationUseCase = mockk()
+    private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk()
+    private val fakeToggleNotificationUseCase = mockk()
     private val fakeUnregisterUnifiedPushUseCase = mockk()
 
     private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
             activeSessionHolder = fakeActiveSessionHolder.instance,
             pushersManager = fakePushersManager.instance,
-            checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase,
-            togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
+            checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
+            toggleNotificationUseCase = fakeToggleNotificationUseCase,
             unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
     )
 
@@ -51,14 +53,14 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
         // Given
         val fakeSession = fakeActiveSessionHolder.fakeSession
         fakeSession.givenSessionId(A_SESSION_ID)
-        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true
-        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
+        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true
+        coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) }
 
         // When
         disableNotificationsForCurrentSessionUseCase.execute()
 
         // Then
-        coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) }
+        coVerify { fakeToggleNotificationUseCase.execute(A_SESSION_ID, false) }
     }
 
     @Test
@@ -66,8 +68,8 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
         // Given
         val fakeSession = fakeActiveSessionHolder.fakeSession
         fakeSession.givenSessionId(A_SESSION_ID)
-        every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false
-        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
+        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false
+        coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) }
         coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
 
         // When
@@ -75,7 +77,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
 
         // Then
         coVerify {
-            fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false)
+            fakeToggleNotificationUseCase.execute(A_SESSION_ID, false)
             fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
         }
     }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
index c923f0c7d6..f10b0777cb 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
@@ -18,7 +18,7 @@ package im.vector.app.features.settings.notifications
 
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
 import io.mockk.coJustRun
@@ -36,14 +36,14 @@ class EnableNotificationsForCurrentSessionUseCaseTest {
 
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakePushersManager = FakePushersManager()
-    private val fakeTogglePushNotificationUseCase = mockk()
+    private val fakeToggleNotificationUseCase = mockk()
     private val fakeRegisterUnifiedPushUseCase = mockk()
     private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk()
 
     private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase(
             activeSessionHolder = fakeActiveSessionHolder.instance,
             pushersManager = fakePushersManager.instance,
-            togglePushNotificationUseCase = fakeTogglePushNotificationUseCase,
+            toggleNotificationUseCase = fakeToggleNotificationUseCase,
             registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
             ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
     )
@@ -57,7 +57,7 @@ class EnableNotificationsForCurrentSessionUseCaseTest {
         fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
         every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
         justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
-        coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) }
+        coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) }
 
         // When
         val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
index bfbbb87705..cc42193fe1 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
@@ -16,14 +16,14 @@
 
 package im.vector.app.test.fakes
 
-import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import io.mockk.coJustRun
 import io.mockk.coVerify
 import io.mockk.mockk
 
 class FakeTogglePushNotificationUseCase {
 
-    val instance = mockk {
+    val instance = mockk {
         coJustRun { execute(any(), any()) }
     }
 

From ab6a6b53c88b4336bb3b96edced8beb852399c1f Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 18 Nov 2022 15:19:34 +0100
Subject: [PATCH 475/679] Some refactorings + update unit tests

---
 .../ConfigureAndStartSessionUseCase.kt        | 17 +++-
 ...oggleNotificationsViaAccountDataUseCase.kt | 14 +--
 ...eNotificationSettingsAccountDataUseCase.kt | 11 ++-
 ...tNotificationSettingsAccountDataUseCase.kt | 34 +++++++
 ...tNotificationSettingsAccountDataUseCase.kt |  9 +-
 .../notification/ToggleNotificationUseCase.kt |  2 +-
 ...NotificationSettingsAccountDataUseCase.kt} | 21 ++--
 .../ConfigureAndStartSessionUseCaseTest.kt    | 56 +++++++----
 ...eNotificationsViaAccountDataUseCaseTest.kt | 32 +++---
 ...ificationSettingsAccountDataUseCaseTest.kt | 59 -----------
 ...ificationSettingsAccountDataUseCaseTest.kt |  9 +-
 ...ificationSettingsAccountDataUseCaseTest.kt | 49 ++++++++++
 .../GetNotificationsStatusUseCaseTest.kt      |  4 +-
 ...ificationSettingsAccountDataUseCaseTest.kt | 14 +--
 .../ToggleNotificationUseCaseTest.kt          |  8 +-
 ...ificationSettingsAccountDataUseCaseTest.kt | 97 +++++++++++++++++++
 .../overview/SessionOverviewViewModelTest.kt  |  8 +-
 ...tificationsForCurrentSessionUseCaseTest.kt |  4 +-
 ...se.kt => FakeToggleNotificationUseCase.kt} |  2 +-
 .../app/test/fakes/FakeVectorPreferences.kt   |  4 +
 20 files changed, 300 insertions(+), 154 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt
 rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{CreateNotificationSettingsAccountDataUseCase.kt => UpdateNotificationSettingsAccountDataUseCase.kt} (57%)
 delete mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
 rename vector/src/test/java/im/vector/app/test/fakes/{FakeTogglePushNotificationUseCase.kt => FakeToggleNotificationUseCase.kt} (96%)

diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
index 96c3f8a6ce..d9688a45ed 100644
--- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
@@ -25,6 +25,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
 import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.settings.VectorPreferences
 import im.vector.app.features.sync.SyncUtils
+import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
 import timber.log.Timber
@@ -36,6 +37,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
         private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
         private val vectorPreferences: VectorPreferences,
         private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater,
+        private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase,
 ) {
 
     fun execute(session: Session, startSyncing: Boolean = true) {
@@ -49,11 +51,24 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
         }
         session.pushersService().refreshPushers()
         webRtcCallManager.checkForProtocolsSupportIfNeeded()
+        updateMatrixClientInfoIfNeeded(session)
+        createNotificationSettingsAccountDataIfNeeded(session)
+        enableNotificationsSettingUpdater.onSessionsStarted(session)
+    }
+
+    private fun updateMatrixClientInfoIfNeeded(session: Session) {
         session.coroutineScope.launch {
             if (vectorPreferences.isClientInfoRecordingEnabled()) {
                 updateMatrixClientInfoUseCase.execute(session)
             }
         }
-        enableNotificationsSettingUpdater.onSessionsStarted(session)
+    }
+
+    private fun createNotificationSettingsAccountDataIfNeeded(session: Session) {
+        session.coroutineScope.launch {
+            if (vectorPreferences.isBackgroundSyncEnabled()) {
+                updateNotificationSettingsAccountDataUseCase.execute(session)
+            }
+        }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt
index b006e3da45..58289495a4 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt
@@ -16,20 +16,14 @@
 
 package im.vector.app.features.settings.devices.v2.notification
 
-import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
-import org.matrix.android.sdk.api.session.events.model.toModel
 import javax.inject.Inject
 
-class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor() {
+class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor(
+        private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase,
+) {
 
     fun execute(session: Session, deviceId: String): Boolean {
-        return session
-                .accountDataService()
-                .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
-                ?.content
-                .toModel()
-                ?.isSilenced != null
+        return getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced != null
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
index 51c24e500c..d71eebdf8a 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
@@ -17,17 +17,22 @@
 package im.vector.app.features.settings.devices.v2.notification
 
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.Session
 import javax.inject.Inject
 
+/**
+ * Delete the content of any associated notification settings to the current session.
+ */
 class DeleteNotificationSettingsAccountDataUseCase @Inject constructor(
         private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
 ) {
 
-    // TODO to be called when switching to push notifications method
-    suspend fun execute(deviceId: String) {
+    // TODO to be called when switching to push notifications method (check notification method setting)
+    suspend fun execute(session: Session) {
+        val deviceId = session.sessionParams.deviceId ?: return
         val emptyNotificationSettingsContent = LocalNotificationSettingsContent(
                 isSilenced = null
         )
-        setNotificationSettingsAccountDataUseCase.execute(deviceId, emptyNotificationSettingsContent)
+        setNotificationSettingsAccountDataUseCase.execute(session, deviceId, emptyNotificationSettingsContent)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt
new file mode 100644
index 0000000000..5517fa0978
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toModel
+import javax.inject.Inject
+
+class GetNotificationSettingsAccountDataUseCase @Inject constructor() {
+
+    fun execute(session: Session, deviceId: String): LocalNotificationSettingsContent? {
+        return session
+                .accountDataService()
+                .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
+                ?.content
+                .toModel()
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt
index f0ec9d5ddc..7306794f16 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt
@@ -16,18 +16,15 @@
 
 package im.vector.app.features.settings.devices.v2.notification
 
-import im.vector.app.core.di.ActiveSessionHolder
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.events.model.toContent
 import javax.inject.Inject
 
-class SetNotificationSettingsAccountDataUseCase @Inject constructor(
-        private val activeSessionHolder: ActiveSessionHolder,
-) {
+class SetNotificationSettingsAccountDataUseCase @Inject constructor() {
 
-    suspend fun execute(deviceId: String, localNotificationSettingsContent: LocalNotificationSettingsContent) {
-        val session = activeSessionHolder.getSafeActiveSession() ?: return
+    suspend fun execute(session: Session, deviceId: String, localNotificationSettingsContent: LocalNotificationSettingsContent) {
         session.accountDataService().updateUserAccountData(
                 UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId,
                 localNotificationSettingsContent.toContent(),
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
index d0e1ea2a7a..73a81a6de1 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
@@ -39,7 +39,7 @@ class ToggleNotificationUseCase @Inject constructor(
 
         if (checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId)) {
             val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
-            setNotificationSettingsAccountDataUseCase.execute(deviceId, newNotificationSettingsContent)
+            setNotificationSettingsAccountDataUseCase.execute(session, deviceId, newNotificationSettingsContent)
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
similarity index 57%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
index e2ee19e5cd..596be90abb 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
@@ -21,18 +21,25 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import org.matrix.android.sdk.api.session.Session
 import javax.inject.Inject
 
-class CreateNotificationSettingsAccountDataUseCase @Inject constructor(
+/**
+ * Update the notification settings account data for the current session.
+ */
+class UpdateNotificationSettingsAccountDataUseCase @Inject constructor(
         private val vectorPreferences: VectorPreferences,
+        private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase,
         private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase
 ) {
 
-    // TODO to be called on session start when background sync is enabled + when switching to background sync
+    // TODO to be called when switching to background sync (in notification method setting)
     suspend fun execute(session: Session) {
         val deviceId = session.sessionParams.deviceId ?: return
-        val isSilenced = !vectorPreferences.areNotificationEnabledForDevice()
-        val notificationSettingsContent = LocalNotificationSettingsContent(
-                isSilenced = isSilenced
-        )
-        setNotificationSettingsAccountDataUseCase.execute(deviceId, notificationSettingsContent)
+        val isSilencedLocal = !vectorPreferences.areNotificationEnabledForDevice()
+        val isSilencedRemote = getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced
+        if (isSilencedLocal != isSilencedRemote) {
+            val notificationSettingsContent = LocalNotificationSettingsContent(
+                    isSilenced = isSilencedLocal
+            )
+            setNotificationSettingsAccountDataUseCase.execute(session, deviceId, notificationSettingsContent)
+        }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
index 01596e796d..23a3629efe 100644
--- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
@@ -20,6 +20,7 @@ import im.vector.app.core.extensions.startSyncing
 import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.sync.SyncUtils
+import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
 import im.vector.app.test.fakes.FakeContext
 import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater
 import im.vector.app.test.fakes.FakeSession
@@ -47,6 +48,7 @@ class ConfigureAndStartSessionUseCaseTest {
     private val fakeUpdateMatrixClientInfoUseCase = mockk()
     private val fakeVectorPreferences = FakeVectorPreferences()
     private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater()
+    private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk()
 
     private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase(
             context = fakeContext.instance,
@@ -54,6 +56,7 @@ class ConfigureAndStartSessionUseCaseTest {
             updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase,
             vectorPreferences = fakeVectorPreferences.instance,
             enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance,
+            updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase,
     )
 
     @Before
@@ -68,47 +71,55 @@ class ConfigureAndStartSessionUseCaseTest {
     }
 
     @Test
-    fun `given start sync needed and client info recording enabled when execute then it should be configured properly`() = runTest {
+    fun `given start sync needed and enabled related preferences when execute then it should be configured properly`() = runTest {
         // Given
-        val fakeSession = givenASession()
-        every { fakeSession.coroutineScope } returns this
+        val aSession = givenASession()
+        every { aSession.coroutineScope } returns this
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
         coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
+        coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
-        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
+        fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true)
+        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession)
 
         // When
-        configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
+        configureAndStartSessionUseCase.execute(aSession, startSyncing = true)
         advanceUntilIdle()
 
         // Then
-        verify { fakeSession.startSyncing(fakeContext.instance) }
-        fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
-        fakeSession.fakePushersService.verifyRefreshPushers()
+        verify { aSession.startSyncing(fakeContext.instance) }
+        aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
+        aSession.fakePushersService.verifyRefreshPushers()
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
-        coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
+        coVerify {
+            fakeUpdateMatrixClientInfoUseCase.execute(aSession)
+            fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession)
+        }
     }
 
     @Test
-    fun `given start sync needed and client info recording disabled when execute then it should be configured properly`() = runTest {
+    fun `given start sync needed and disabled related preferences when execute then it should be configured properly`() = runTest {
         // Given
-        val fakeSession = givenASession()
-        every { fakeSession.coroutineScope } returns this
+        val aSession = givenASession()
+        every { aSession.coroutineScope } returns this
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
-        coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false)
-        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
+        fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = false)
+        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession)
 
         // When
-        configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
+        configureAndStartSessionUseCase.execute(aSession, startSyncing = true)
         advanceUntilIdle()
 
         // Then
-        verify { fakeSession.startSyncing(fakeContext.instance) }
-        fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
-        fakeSession.fakePushersService.verifyRefreshPushers()
+        verify { aSession.startSyncing(fakeContext.instance) }
+        aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
+        aSession.fakePushersService.verifyRefreshPushers()
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
-        coVerify(inverse = true) { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
+        coVerify(inverse = true) {
+            fakeUpdateMatrixClientInfoUseCase.execute(aSession)
+            fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession)
+        }
     }
 
     @Test
@@ -118,7 +129,9 @@ class ConfigureAndStartSessionUseCaseTest {
         every { fakeSession.coroutineScope } returns this
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
         coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
+        coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
+        fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true)
         fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
 
         // When
@@ -130,7 +143,10 @@ class ConfigureAndStartSessionUseCaseTest {
         fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
         fakeSession.fakePushersService.verifyRefreshPushers()
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
-        coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
+        coVerify {
+            fakeUpdateMatrixClientInfoUseCase.execute(fakeSession)
+            fakeUpdateNotificationSettingsAccountDataUseCase.execute(fakeSession)
+        }
     }
 
     private fun givenASession(): FakeSession {
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt
index de225d36ac..f97e326a02 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt
@@ -17,31 +17,29 @@
 package im.vector.app.features.settings.devices.v2.notification
 
 import im.vector.app.test.fakes.FakeSession
+import io.mockk.every
 import io.mockk.mockk
 import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Test
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
-import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
-import org.matrix.android.sdk.api.session.events.model.toContent
 
 private const val A_DEVICE_ID = "device-id"
 
 class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest {
 
+    private val fakeGetNotificationSettingsAccountDataUseCase = mockk()
     private val fakeSession = FakeSession()
 
     private val checkIfCanToggleNotificationsViaAccountDataUseCase =
-            CheckIfCanToggleNotificationsViaAccountDataUseCase()
+            CheckIfCanToggleNotificationsViaAccountDataUseCase(
+                    getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase,
+            )
 
     @Test
     fun `given current session and an account data with a content for the device id when execute then result is true`() {
         // Given
-        fakeSession
-                .accountDataService()
-                .givenGetUserAccountDataEventReturns(
-                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
-                        content = LocalNotificationSettingsContent(isSilenced = true).toContent(),
-                )
+        val content = LocalNotificationSettingsContent(isSilenced = true)
+        every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content
 
         // When
         val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
@@ -53,12 +51,8 @@ class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest {
     @Test
     fun `given current session and an account data with empty content for the device id when execute then result is false`() {
         // Given
-        fakeSession
-                .accountDataService()
-                .givenGetUserAccountDataEventReturns(
-                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
-                        content = mockk(),
-                )
+        val content = LocalNotificationSettingsContent(isSilenced = null)
+        every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content
 
         // When
         val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
@@ -70,12 +64,8 @@ class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest {
     @Test
     fun `given current session and NO account data for the device id when execute then result is false`() {
         // Given
-        fakeSession
-                .accountDataService()
-                .givenGetUserAccountDataEventReturns(
-                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID,
-                        content = null,
-                )
+        val content = null
+        every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content
 
         // When
         val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt
deleted file mode 100644
index e4cadaa005..0000000000
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CreateNotificationSettingsAccountDataUseCaseTest.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (c) 2022 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.app.features.settings.devices.v2.notification
-
-import im.vector.app.test.fakes.FakeSession
-import im.vector.app.test.fakes.FakeVectorPreferences
-import io.mockk.coJustRun
-import io.mockk.coVerify
-import io.mockk.mockk
-import io.mockk.verify
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
-
-class CreateNotificationSettingsAccountDataUseCaseTest {
-
-    private val fakeVectorPreferences = FakeVectorPreferences()
-    private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
-
-    private val createNotificationSettingsAccountDataUseCase = CreateNotificationSettingsAccountDataUseCase(
-        vectorPreferences = fakeVectorPreferences.instance,
-        setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
-    )
-
-    @Test
-    fun `given a device id when execute then content with the current notification preference is set for the account data`() = runTest {
-        // Given
-        val aDeviceId = "device-id"
-        val session = FakeSession()
-        session.givenSessionId(aDeviceId)
-        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) }
-        val areNotificationsEnabled = true
-        fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
-        val expectedContent = LocalNotificationSettingsContent(
-                isSilenced = !areNotificationsEnabled
-        )
-
-        // When
-        createNotificationSettingsAccountDataUseCase.execute(session)
-
-        // Then
-        verify { fakeVectorPreferences.instance.areNotificationEnabledForDevice() }
-        coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) }
-    }
-}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
index 038a1f436a..d84ff8c6ac 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.settings.devices.v2.notification
 
+import im.vector.app.test.fakes.FakeSession
 import io.mockk.coJustRun
 import io.mockk.coVerify
 import io.mockk.mockk
@@ -35,15 +36,17 @@ class DeleteNotificationSettingsAccountDataUseCaseTest {
     fun `given a device id when execute then empty content is set for the account data`() = runTest {
         // Given
         val aDeviceId = "device-id"
-        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) }
+        val aSession = FakeSession()
+        aSession.givenSessionId(aDeviceId)
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val expectedContent = LocalNotificationSettingsContent(
                 isSilenced = null
         )
 
         // When
-        deleteNotificationSettingsAccountDataUseCase.execute(aDeviceId)
+        deleteNotificationSettingsAccountDataUseCase.execute(aSession)
 
         // Then
-        coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aDeviceId, expectedContent) }
+        coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt
new file mode 100644
index 0000000000..75179b5679
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeSession
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toContent
+
+class GetNotificationSettingsAccountDataUseCaseTest {
+
+    private val getNotificationSettingsAccountDataUseCase = GetNotificationSettingsAccountDataUseCase()
+
+    @Test
+    fun `given a device id when execute then retrieve the account data event corresponding to this id if any`() {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        val expectedContent = LocalNotificationSettingsContent()
+        aSession
+                .accountDataService()
+                .givenGetUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId,
+                        content = expectedContent.toContent(),
+                )
+
+        // When
+        val result = getNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
+
+        // Then
+        result shouldBeEqualTo expectedContent
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
index 7c0b0a8693..d4c3aa5788 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
@@ -53,8 +53,8 @@ class GetNotificationsStatusUseCaseTest {
 
     private val getNotificationsStatusUseCase =
             GetNotificationsStatusUseCase(
-                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
-                    canTogglePushNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase,
+                    checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
+                    canToggleNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase,
             )
 
     @Before
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
index 8f72f0946f..d26271e59d 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
@@ -16,7 +16,7 @@
 
 package im.vector.app.features.settings.devices.v2.notification
 
-import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeSession
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
@@ -25,25 +25,21 @@ import org.matrix.android.sdk.api.session.events.model.toContent
 
 class SetNotificationSettingsAccountDataUseCaseTest {
 
-    private val activeSessionHolder = FakeActiveSessionHolder()
-
-    private val setNotificationSettingsAccountDataUseCase = SetNotificationSettingsAccountDataUseCase(
-            activeSessionHolder = activeSessionHolder.instance,
-    )
+    private val setNotificationSettingsAccountDataUseCase = SetNotificationSettingsAccountDataUseCase()
 
     @Test
     fun `given a content when execute then update local notification settings with this content`() = runTest {
         // Given
         val sessionId = "a_session_id"
         val localNotificationSettingsContent = LocalNotificationSettingsContent()
-        val fakeSession = activeSessionHolder.fakeSession
+        val fakeSession = FakeSession()
         fakeSession.accountDataService().givenUpdateUserAccountDataEventSucceeds()
 
         // When
-        setNotificationSettingsAccountDataUseCase.execute(sessionId, localNotificationSettingsContent)
+        setNotificationSettingsAccountDataUseCase.execute(fakeSession, sessionId, localNotificationSettingsContent)
 
         // Then
-        activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds(
+        fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds(
                 UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId,
                 localNotificationSettingsContent.toContent(),
         )
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
index 99be7c7ea9..1e3517c776 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
@@ -39,8 +39,8 @@ class ToggleNotificationUseCaseTest {
     private val toggleNotificationUseCase =
             ToggleNotificationUseCase(
                     activeSessionHolder = activeSessionHolder.instance,
-                    checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
-                    checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
+                    checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
+                    checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
                     setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
             )
 
@@ -72,7 +72,7 @@ class ToggleNotificationUseCaseTest {
         val fakeSession = activeSessionHolder.fakeSession
         every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false
         every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true
-        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any()) }
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val expectedLocalNotificationSettingsContent = LocalNotificationSettingsContent(
                 isSilenced = false
         )
@@ -82,7 +82,7 @@ class ToggleNotificationUseCaseTest {
 
         // Then
         coVerify {
-            fakeSetNotificationSettingsAccountDataUseCase.execute(sessionId, expectedLocalNotificationSettingsContent)
+            fakeSetNotificationSettingsAccountDataUseCase.execute(fakeSession, sessionId, expectedLocalNotificationSettingsContent)
         }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
new file mode 100644
index 0000000000..41c5ab9081
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeVectorPreferences
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+
+class UpdateNotificationSettingsAccountDataUseCaseTest {
+
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeGetNotificationSettingsAccountDataUseCase = mockk()
+    private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
+
+    private val updateNotificationSettingsAccountDataUseCase = UpdateNotificationSettingsAccountDataUseCase(
+            vectorPreferences = fakeVectorPreferences.instance,
+            getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase,
+            setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
+    )
+
+    @Test
+    fun `given a device id and a different local setting compared to remote when execute then content is updated`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        aSession.givenSessionId(aDeviceId)
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
+        val areNotificationsEnabled = true
+        fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
+        every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
+                LocalNotificationSettingsContent(
+                        isSilenced = null
+                )
+        val expectedContent = LocalNotificationSettingsContent(
+                isSilenced = !areNotificationsEnabled
+        )
+
+        // When
+        updateNotificationSettingsAccountDataUseCase.execute(aSession)
+
+        // Then
+        verify {
+            fakeVectorPreferences.instance.areNotificationEnabledForDevice()
+            fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
+        }
+        coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) }
+    }
+
+    @Test
+    fun `given a device id and a same local setting compared to remote when execute then content is not updated`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        aSession.givenSessionId(aDeviceId)
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
+        val areNotificationsEnabled = true
+        fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
+        every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
+                LocalNotificationSettingsContent(
+                        isSilenced = false
+                )
+        val expectedContent = LocalNotificationSettingsContent(
+                isSilenced = !areNotificationsEnabled
+        )
+
+        // When
+        updateNotificationSettingsAccountDataUseCase.execute(aSession)
+
+        // Then
+        verify {
+            fakeVectorPreferences.instance.areNotificationEnabledForDevice()
+            fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
+        }
+        coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
index 287bdd159c..901c0331c5 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
@@ -30,7 +30,7 @@ import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase
 import im.vector.app.test.fakes.FakePendingAuthHandler
 import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
-import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase
+import im.vector.app.test.fakes.FakeToggleNotificationUseCase
 import im.vector.app.test.fakes.FakeVectorPreferences
 import im.vector.app.test.fakes.FakeVerificationService
 import im.vector.app.test.test
@@ -76,7 +76,7 @@ class SessionOverviewViewModelTest {
     private val interceptSignoutFlowResponseUseCase = mockk()
     private val fakePendingAuthHandler = FakePendingAuthHandler()
     private val refreshDevicesUseCase = mockk(relaxed = true)
-    private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
+    private val toggleNotificationUseCase = FakeToggleNotificationUseCase()
     private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase()
     private val notificationsStatus = NotificationsStatus.ENABLED
     private val fakeVectorPreferences = FakeVectorPreferences()
@@ -91,7 +91,7 @@ class SessionOverviewViewModelTest {
             pendingAuthHandler = fakePendingAuthHandler.instance,
             activeSessionHolder = fakeActiveSessionHolder.instance,
             refreshDevicesUseCase = refreshDevicesUseCase,
-            togglePushNotificationUseCase = togglePushNotificationUseCase.instance,
+            toggleNotificationUseCase = toggleNotificationUseCase.instance,
             getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance,
             vectorPreferences = fakeVectorPreferences.instance,
             toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase,
@@ -436,7 +436,7 @@ class SessionOverviewViewModelTest {
 
         viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true))
 
-        togglePushNotificationUseCase.verifyExecute(A_SESSION_ID_1, true)
+        toggleNotificationUseCase.verifyExecute(A_SESSION_ID_1, true)
         viewModel.test().assertLatestState { state -> state.notificationsStatus == NotificationsStatus.ENABLED }.finish()
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
index 153b79f1a8..e53874858a 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -16,11 +16,9 @@
 
 package im.vector.app.features.settings.notifications
 
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
 import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
-import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase
-import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
 import io.mockk.coJustRun
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt
similarity index 96%
rename from vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
rename to vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt
index cc42193fe1..527625144e 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt
@@ -21,7 +21,7 @@ import io.mockk.coJustRun
 import io.mockk.coVerify
 import io.mockk.mockk
 
-class FakeTogglePushNotificationUseCase {
+class FakeToggleNotificationUseCase {
 
     val instance = mockk {
         coJustRun { execute(any(), any()) }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 06efca1bf7..58bc1a18b8 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -73,4 +73,8 @@ class FakeVectorPreferences {
     fun givenAreNotificationsEnabledForDevice(notificationsEnabled: Boolean) {
         every { instance.areNotificationEnabledForDevice() } returns notificationsEnabled
     }
+
+    fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) {
+        every { instance.isBackgroundSyncEnabled() } returns isEnabled
+    }
 }

From e99dc1d163008123520b5e9cfb58d7ac7795fef9 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 18 Nov 2022 17:17:05 +0100
Subject: [PATCH 476/679] Remove unused parameters from some ViewModel

---
 .../vector/app/features/settings/devices/v2/DevicesViewModel.kt | 1 -
 .../settings/devices/v2/overview/SessionOverviewViewModel.kt    | 1 -
 .../app/features/settings/devices/v2/DevicesViewModelTest.kt    | 2 --
 .../devices/v2/overview/SessionOverviewViewModelTest.kt         | 2 --
 4 files changed, 6 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
index f42d5af398..bfccd2f9d3 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
@@ -48,7 +48,6 @@ class DevicesViewModel @AssistedInject constructor(
         private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
         private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
         private val signoutSessionsUseCase: SignoutSessionsUseCase,
-        private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
         private val pendingAuthHandler: PendingAuthHandler,
         refreshDevicesUseCase: RefreshDevicesUseCase,
         private val vectorPreferences: VectorPreferences,
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
index 0ddf688514..74f962b464 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
@@ -51,7 +51,6 @@ class SessionOverviewViewModel @AssistedInject constructor(
         private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
         private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
         private val signoutSessionsUseCase: SignoutSessionsUseCase,
-        private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
         private val pendingAuthHandler: PendingAuthHandler,
         private val activeSessionHolder: ActiveSessionHolder,
         private val toggleNotificationUseCase: ToggleNotificationUseCase,
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
index 03177aac47..aa5ebd73eb 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
@@ -72,7 +72,6 @@ class DevicesViewModelTest {
     private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true)
     private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk()
     private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
-    private val fakeInterceptSignoutFlowResponseUseCase = mockk()
     private val fakePendingAuthHandler = FakePendingAuthHandler()
     private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true)
     private val fakeVectorPreferences = FakeVectorPreferences()
@@ -87,7 +86,6 @@ class DevicesViewModelTest {
                 refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase,
                 checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
                 signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
-                interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
                 pendingAuthHandler = fakePendingAuthHandler.instance,
                 refreshDevicesUseCase = fakeRefreshDevicesUseCase,
                 vectorPreferences = fakeVectorPreferences.instance,
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
index 901c0331c5..2ddc91cdac 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
@@ -73,7 +73,6 @@ class SessionOverviewViewModelTest {
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk()
     private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
-    private val interceptSignoutFlowResponseUseCase = mockk()
     private val fakePendingAuthHandler = FakePendingAuthHandler()
     private val refreshDevicesUseCase = mockk(relaxed = true)
     private val toggleNotificationUseCase = FakeToggleNotificationUseCase()
@@ -87,7 +86,6 @@ class SessionOverviewViewModelTest {
             getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
             checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
             signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
-            interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase,
             pendingAuthHandler = fakePendingAuthHandler.instance,
             activeSessionHolder = fakeActiveSessionHolder.instance,
             refreshDevicesUseCase = refreshDevicesUseCase,

From 637961bbb1043fd39ac0844d09eeb65dc7b30f0a Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 23 Nov 2022 14:45:37 +0100
Subject: [PATCH 477/679] Update related account data event on notification
 method change

---
 .../LocalNotificationSettingsContent.kt       |  2 +-
 .../EnableNotificationsSettingUpdater.kt      | 39 ----------
 .../NotificationsSettingUpdater.kt            | 74 +++++++++++++++++++
 .../ConfigureAndStartSessionUseCase.kt        | 10 +--
 ...eNotificationSettingsAccountDataUseCase.kt | 12 +--
 ...eNotificationSettingsAccountDataUseCase.kt | 27 +++++--
 .../ConfigureAndStartSessionUseCaseTest.kt    | 38 +++++-----
 ...ificationSettingsAccountDataUseCaseTest.kt | 28 ++++++-
 ...ificationSettingsAccountDataUseCaseTest.kt | 22 +++++-
 ...ificationSettingsAccountDataUseCaseTest.kt |  2 +-
 ...ificationSettingsAccountDataUseCaseTest.kt | 32 +++++++-
 ....kt => FakeNotificationsSettingUpdater.kt} |  6 +-
 12 files changed, 207 insertions(+), 85 deletions(-)
 delete mode 100644 vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt
 create mode 100644 vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
 rename vector/src/test/java/im/vector/app/test/fakes/{FakeEnableNotificationsSettingUpdater.kt => FakeNotificationsSettingUpdater.kt} (82%)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
index 6998d9dcf2..75d04f340a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt
@@ -22,5 +22,5 @@ import com.squareup.moshi.JsonClass
 @JsonClass(generateAdapter = true)
 data class LocalNotificationSettingsContent(
         @Json(name = "is_silenced")
-        val isSilenced: Boolean? = false
+        val isSilenced: Boolean?
 )
diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt
deleted file mode 100644
index 81b524cde9..0000000000
--- a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (c) 2022 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.app.core.notification
-
-import im.vector.app.features.session.coroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.session.Session
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class EnableNotificationsSettingUpdater @Inject constructor(
-        private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase,
-) {
-
-    private var job: Job? = null
-
-    fun onSessionsStarted(session: Session) {
-        job?.cancel()
-        job = session.coroutineScope.launch {
-            updateEnableNotificationsSettingOnChangeUseCase.execute(session)
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
new file mode 100644
index 0000000000..c6738edddb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.notification
+
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import im.vector.app.features.session.coroutineScope
+import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_FDROID_BACKGROUND_SYNC_MODE
+import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Listen changes in Pusher or Account Data to update the local setting for notification toggle.
+ * Listen changes on background sync mode preference to update the corresponding Account Data event.
+ */
+@Singleton
+class NotificationsSettingUpdater @Inject constructor(
+        private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase,
+        private val vectorPreferences: VectorPreferences,
+        private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase,
+) {
+
+    private var job: Job? = null
+    private var prefChangeListener: OnSharedPreferenceChangeListener? = null
+
+    // TODO add unit tests
+    fun onSessionsStarted(session: Session) {
+        updateEnableNotificationsSettingOnChange(session)
+        updateNotificationSettingsAccountDataOnChange(session)
+    }
+
+    private fun updateEnableNotificationsSettingOnChange(session: Session) {
+        job?.cancel()
+        job = session.coroutineScope.launch {
+            updateEnableNotificationsSettingOnChangeUseCase.execute(session)
+        }
+    }
+
+    private fun updateNotificationSettingsAccountDataOnChange(session: Session) {
+        prefChangeListener?.let { vectorPreferences.unsubscribeToChanges(it) }
+        prefChangeListener = null
+        prefChangeListener = createPrefListener(session).also {
+            vectorPreferences.subscribeToChanges(it)
+        }
+    }
+
+    private fun createPrefListener(session: Session): OnSharedPreferenceChangeListener {
+        return OnSharedPreferenceChangeListener { _, key ->
+            session.coroutineScope.launch {
+                if (key == SETTINGS_FDROID_BACKGROUND_SYNC_MODE) {
+                    updateNotificationSettingsAccountDataUseCase.execute(session)
+                }
+            }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
index d9688a45ed..623f7d83a9 100644
--- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
@@ -19,7 +19,7 @@ package im.vector.app.core.session
 import android.content.Context
 import dagger.hilt.android.qualifiers.ApplicationContext
 import im.vector.app.core.extensions.startSyncing
-import im.vector.app.core.notification.EnableNotificationsSettingUpdater
+import im.vector.app.core.notification.NotificationsSettingUpdater
 import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.features.call.webrtc.WebRtcCallManager
 import im.vector.app.features.session.coroutineScope
@@ -36,7 +36,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
         private val webRtcCallManager: WebRtcCallManager,
         private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
         private val vectorPreferences: VectorPreferences,
-        private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater,
+        private val notificationsSettingUpdater: NotificationsSettingUpdater,
         private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase,
 ) {
 
@@ -53,7 +53,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
         webRtcCallManager.checkForProtocolsSupportIfNeeded()
         updateMatrixClientInfoIfNeeded(session)
         createNotificationSettingsAccountDataIfNeeded(session)
-        enableNotificationsSettingUpdater.onSessionsStarted(session)
+        notificationsSettingUpdater.onSessionsStarted(session)
     }
 
     private fun updateMatrixClientInfoIfNeeded(session: Session) {
@@ -66,9 +66,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
 
     private fun createNotificationSettingsAccountDataIfNeeded(session: Session) {
         session.coroutineScope.launch {
-            if (vectorPreferences.isBackgroundSyncEnabled()) {
-                updateNotificationSettingsAccountDataUseCase.execute(session)
-            }
+            updateNotificationSettingsAccountDataUseCase.execute(session)
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
index d71eebdf8a..3c086fe111 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt
@@ -24,15 +24,17 @@ import javax.inject.Inject
  * Delete the content of any associated notification settings to the current session.
  */
 class DeleteNotificationSettingsAccountDataUseCase @Inject constructor(
+        private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase,
         private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
 ) {
 
-    // TODO to be called when switching to push notifications method (check notification method setting)
     suspend fun execute(session: Session) {
         val deviceId = session.sessionParams.deviceId ?: return
-        val emptyNotificationSettingsContent = LocalNotificationSettingsContent(
-                isSilenced = null
-        )
-        setNotificationSettingsAccountDataUseCase.execute(session, deviceId, emptyNotificationSettingsContent)
+        if (getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced != null) {
+            val emptyNotificationSettingsContent = LocalNotificationSettingsContent(
+                    isSilenced = null
+            )
+            setNotificationSettingsAccountDataUseCase.execute(session, deviceId, emptyNotificationSettingsContent)
+        }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
index 596be90abb..7791c1dd4b 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
@@ -22,24 +22,37 @@ import org.matrix.android.sdk.api.session.Session
 import javax.inject.Inject
 
 /**
- * Update the notification settings account data for the current session.
+ * Update the notification settings account data for the current session depending on whether
+ * the background sync is enabled or not.
  */
 class UpdateNotificationSettingsAccountDataUseCase @Inject constructor(
         private val vectorPreferences: VectorPreferences,
         private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase,
-        private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase
+        private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
+        private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase,
 ) {
 
-    // TODO to be called when switching to background sync (in notification method setting)
     suspend fun execute(session: Session) {
+        if (vectorPreferences.isBackgroundSyncEnabled()) {
+            setCurrentNotificationStatus(session)
+        } else {
+            deleteCurrentNotificationStatus(session)
+        }
+    }
+
+    private suspend fun setCurrentNotificationStatus(session: Session) {
         val deviceId = session.sessionParams.deviceId ?: return
-        val isSilencedLocal = !vectorPreferences.areNotificationEnabledForDevice()
-        val isSilencedRemote = getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced
-        if (isSilencedLocal != isSilencedRemote) {
+        val areNotificationsSilenced = !vectorPreferences.areNotificationEnabledForDevice()
+        val isSilencedAccountData = getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced
+        if (areNotificationsSilenced != isSilencedAccountData) {
             val notificationSettingsContent = LocalNotificationSettingsContent(
-                    isSilenced = isSilencedLocal
+                    isSilenced = areNotificationsSilenced
             )
             setNotificationSettingsAccountDataUseCase.execute(session, deviceId, notificationSettingsContent)
         }
     }
+
+    private suspend fun deleteCurrentNotificationStatus(session: Session) {
+        deleteNotificationSettingsAccountDataUseCase.execute(session)
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
index 23a3629efe..4071afaf3f 100644
--- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
@@ -22,7 +22,7 @@ import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.sync.SyncUtils
 import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
 import im.vector.app.test.fakes.FakeContext
-import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater
+import im.vector.app.test.fakes.FakeNotificationsSettingUpdater
 import im.vector.app.test.fakes.FakeSession
 import im.vector.app.test.fakes.FakeVectorPreferences
 import im.vector.app.test.fakes.FakeWebRtcCallManager
@@ -47,7 +47,7 @@ class ConfigureAndStartSessionUseCaseTest {
     private val fakeWebRtcCallManager = FakeWebRtcCallManager()
     private val fakeUpdateMatrixClientInfoUseCase = mockk()
     private val fakeVectorPreferences = FakeVectorPreferences()
-    private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater()
+    private val fakeNotificationsSettingUpdater = FakeNotificationsSettingUpdater()
     private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk()
 
     private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase(
@@ -55,7 +55,7 @@ class ConfigureAndStartSessionUseCaseTest {
             webRtcCallManager = fakeWebRtcCallManager.instance,
             updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase,
             vectorPreferences = fakeVectorPreferences.instance,
-            enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance,
+            notificationsSettingUpdater = fakeNotificationsSettingUpdater.instance,
             updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase,
     )
 
@@ -71,7 +71,7 @@ class ConfigureAndStartSessionUseCaseTest {
     }
 
     @Test
-    fun `given start sync needed and enabled related preferences when execute then it should be configured properly`() = runTest {
+    fun `given start sync needed and client info recording enabled when execute then it should be configured properly`() = runTest {
         // Given
         val aSession = givenASession()
         every { aSession.coroutineScope } returns this
@@ -79,8 +79,7 @@ class ConfigureAndStartSessionUseCaseTest {
         coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
         coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
-        fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true)
-        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession)
+        fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession)
 
         // When
         configureAndStartSessionUseCase.execute(aSession, startSyncing = true)
@@ -98,14 +97,14 @@ class ConfigureAndStartSessionUseCaseTest {
     }
 
     @Test
-    fun `given start sync needed and disabled related preferences when execute then it should be configured properly`() = runTest {
+    fun `given start sync needed and client info recording disabled when execute then it should be configured properly`() = runTest {
         // Given
         val aSession = givenASession()
         every { aSession.coroutineScope } returns this
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
+        coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false)
-        fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = false)
-        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(aSession)
+        fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession)
 
         // When
         configureAndStartSessionUseCase.execute(aSession, startSyncing = true)
@@ -118,6 +117,8 @@ class ConfigureAndStartSessionUseCaseTest {
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
         coVerify(inverse = true) {
             fakeUpdateMatrixClientInfoUseCase.execute(aSession)
+        }
+        coVerify {
             fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession)
         }
     }
@@ -125,27 +126,26 @@ class ConfigureAndStartSessionUseCaseTest {
     @Test
     fun `given a session and no start sync needed when execute then it should be configured properly`() = runTest {
         // Given
-        val fakeSession = givenASession()
-        every { fakeSession.coroutineScope } returns this
+        val aSession = givenASession()
+        every { aSession.coroutineScope } returns this
         fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
         coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
         coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
         fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true)
-        fakeVectorPreferences.givenIsBackgroundSyncEnabled(isEnabled = true)
-        fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession)
+        fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession)
 
         // When
-        configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false)
+        configureAndStartSessionUseCase.execute(aSession, startSyncing = false)
         advanceUntilIdle()
 
         // Then
-        verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) }
-        fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
-        fakeSession.fakePushersService.verifyRefreshPushers()
+        verify(inverse = true) { aSession.startSyncing(fakeContext.instance) }
+        aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder())
+        aSession.fakePushersService.verifyRefreshPushers()
         fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
         coVerify {
-            fakeUpdateMatrixClientInfoUseCase.execute(fakeSession)
-            fakeUpdateNotificationSettingsAccountDataUseCase.execute(fakeSession)
+            fakeUpdateMatrixClientInfoUseCase.execute(aSession)
+            fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession)
         }
     }
 
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
index d84ff8c6ac..600ba2ba48 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt
@@ -19,7 +19,9 @@ package im.vector.app.features.settings.devices.v2.notification
 import im.vector.app.test.fakes.FakeSession
 import io.mockk.coJustRun
 import io.mockk.coVerify
+import io.mockk.every
 import io.mockk.mockk
+import io.mockk.verify
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
@@ -27,17 +29,22 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 class DeleteNotificationSettingsAccountDataUseCaseTest {
 
     private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
+    private val fakeGetNotificationSettingsAccountDataUseCase = mockk()
 
     private val deleteNotificationSettingsAccountDataUseCase = DeleteNotificationSettingsAccountDataUseCase(
             setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
+            getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase,
     )
 
     @Test
-    fun `given a device id when execute then empty content is set for the account data`() = runTest {
+    fun `given a device id and existing account data content when execute then empty content is set for the account data`() = runTest {
         // Given
         val aDeviceId = "device-id"
         val aSession = FakeSession()
         aSession.givenSessionId(aDeviceId)
+        every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent(
+                isSilenced = true,
+        )
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val expectedContent = LocalNotificationSettingsContent(
                 isSilenced = null
@@ -47,6 +54,25 @@ class DeleteNotificationSettingsAccountDataUseCaseTest {
         deleteNotificationSettingsAccountDataUseCase.execute(aSession)
 
         // Then
+        verify { fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) }
         coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) }
     }
+
+    @Test
+    fun `given a device id and empty existing account data content when execute then nothing is done`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        aSession.givenSessionId(aDeviceId)
+        every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent(
+                isSilenced = null,
+        )
+
+        // When
+        deleteNotificationSettingsAccountDataUseCase.execute(aSession)
+
+        // Then
+        verify { fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) }
+        coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) }
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt
index 75179b5679..2adb0d8599 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt
@@ -32,7 +32,27 @@ class GetNotificationSettingsAccountDataUseCaseTest {
         // Given
         val aDeviceId = "device-id"
         val aSession = FakeSession()
-        val expectedContent = LocalNotificationSettingsContent()
+        val expectedContent = LocalNotificationSettingsContent(isSilenced = true)
+        aSession
+                .accountDataService()
+                .givenGetUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId,
+                        content = expectedContent.toContent(),
+                )
+
+        // When
+        val result = getNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
+
+        // Then
+        result shouldBeEqualTo expectedContent
+    }
+
+    @Test
+    fun `given a device id and empty content when execute then retrieve the account data event corresponding to this id if any`() {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        val expectedContent = LocalNotificationSettingsContent(isSilenced = null)
         aSession
                 .accountDataService()
                 .givenGetUserAccountDataEventReturns(
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
index d26271e59d..89fcd5e512 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt
@@ -31,7 +31,7 @@ class SetNotificationSettingsAccountDataUseCaseTest {
     fun `given a content when execute then update local notification settings with this content`() = runTest {
         // Given
         val sessionId = "a_session_id"
-        val localNotificationSettingsContent = LocalNotificationSettingsContent()
+        val localNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = true)
         val fakeSession = FakeSession()
         fakeSession.accountDataService().givenUpdateUserAccountDataEventSucceeds()
 
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
index 41c5ab9081..3bca0da84e 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
@@ -32,15 +32,17 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
     private val fakeVectorPreferences = FakeVectorPreferences()
     private val fakeGetNotificationSettingsAccountDataUseCase = mockk()
     private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
+    private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk()
 
     private val updateNotificationSettingsAccountDataUseCase = UpdateNotificationSettingsAccountDataUseCase(
             vectorPreferences = fakeVectorPreferences.instance,
             getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase,
             setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
+            deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase,
     )
 
     @Test
-    fun `given a device id and a different local setting compared to remote when execute then content is updated`() = runTest {
+    fun `given back sync enabled, a device id and a different local setting compared to remote when execute then content is updated`() = runTest {
         // Given
         val aDeviceId = "device-id"
         val aSession = FakeSession()
@@ -48,6 +50,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val areNotificationsEnabled = true
         fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
+        fakeVectorPreferences.givenIsBackgroundSyncEnabled(true)
         every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
                 LocalNotificationSettingsContent(
                         isSilenced = null
@@ -61,14 +64,16 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
 
         // Then
         verify {
+            fakeVectorPreferences.instance.isBackgroundSyncEnabled()
             fakeVectorPreferences.instance.areNotificationEnabledForDevice()
             fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
         }
+        coVerify(inverse = true) { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) }
         coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) }
     }
 
     @Test
-    fun `given a device id and a same local setting compared to remote when execute then content is not updated`() = runTest {
+    fun `given back sync enabled, a device id and a same local setting compared to remote when execute then content is not updated`() = runTest {
         // Given
         val aDeviceId = "device-id"
         val aSession = FakeSession()
@@ -76,6 +81,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val areNotificationsEnabled = true
         fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
+        fakeVectorPreferences.givenIsBackgroundSyncEnabled(true)
         every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
                 LocalNotificationSettingsContent(
                         isSilenced = false
@@ -89,9 +95,31 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
 
         // Then
         verify {
+            fakeVectorPreferences.instance.isBackgroundSyncEnabled()
             fakeVectorPreferences.instance.areNotificationEnabledForDevice()
             fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
         }
+        coVerify(inverse = true) { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) }
         coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) }
     }
+
+    @Test
+    fun `given back sync disabled and a device id when execute then content is deleted`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        aSession.givenSessionId(aDeviceId)
+        coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) }
+        fakeVectorPreferences.givenIsBackgroundSyncEnabled(false)
+
+        // When
+        updateNotificationSettingsAccountDataUseCase.execute(aSession)
+
+        // Then
+        verify {
+            fakeVectorPreferences.instance.isBackgroundSyncEnabled()
+        }
+        coVerify { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) }
+        coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) }
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt
similarity index 82%
rename from vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt
rename to vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt
index a78dd1a34b..f9f38e6c2a 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt
@@ -16,14 +16,14 @@
 
 package im.vector.app.test.fakes
 
-import im.vector.app.core.notification.EnableNotificationsSettingUpdater
+import im.vector.app.core.notification.NotificationsSettingUpdater
 import io.mockk.justRun
 import io.mockk.mockk
 import org.matrix.android.sdk.api.session.Session
 
-class FakeEnableNotificationsSettingUpdater {
+class FakeNotificationsSettingUpdater {
 
-    val instance = mockk()
+    val instance = mockk()
 
     fun givenOnSessionsStarted(session: Session) {
         justRun { instance.onSessionsStarted(session) }

From 7c10a4cb21a0bc0a044698ea633c5a76626bdbd6 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 23 Nov 2022 15:12:51 +0100
Subject: [PATCH 478/679] Adding tests for notifications setting updater

---
 .../NotificationsSettingUpdater.kt            |   7 +-
 .../ConfigureAndStartSessionUseCase.kt        |   2 +-
 .../NotificationsSettingUpdaterTest.kt        | 106 ++++++++++++++++++
 .../fakes/FakeNotificationsSettingUpdater.kt  |   2 +-
 .../app/test/fakes/FakeVectorPreferences.kt   |   7 ++
 5 files changed, 118 insertions(+), 6 deletions(-)
 create mode 100644 vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt

diff --git a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
index c6738edddb..4a16f37cfe 100644
--- a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
+++ b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
@@ -41,10 +41,9 @@ class NotificationsSettingUpdater @Inject constructor(
     private var job: Job? = null
     private var prefChangeListener: OnSharedPreferenceChangeListener? = null
 
-    // TODO add unit tests
-    fun onSessionsStarted(session: Session) {
+    fun onSessionStarted(session: Session) {
         updateEnableNotificationsSettingOnChange(session)
-        updateNotificationSettingsAccountDataOnChange(session)
+        updateAccountDataOnBackgroundSyncChange(session)
     }
 
     private fun updateEnableNotificationsSettingOnChange(session: Session) {
@@ -54,7 +53,7 @@ class NotificationsSettingUpdater @Inject constructor(
         }
     }
 
-    private fun updateNotificationSettingsAccountDataOnChange(session: Session) {
+    private fun updateAccountDataOnBackgroundSyncChange(session: Session) {
         prefChangeListener?.let { vectorPreferences.unsubscribeToChanges(it) }
         prefChangeListener = null
         prefChangeListener = createPrefListener(session).also {
diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
index 623f7d83a9..d167b02d05 100644
--- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
@@ -53,7 +53,7 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
         webRtcCallManager.checkForProtocolsSupportIfNeeded()
         updateMatrixClientInfoIfNeeded(session)
         createNotificationSettingsAccountDataIfNeeded(session)
-        notificationsSettingUpdater.onSessionsStarted(session)
+        notificationsSettingUpdater.onSessionStarted(session)
     }
 
     private fun updateMatrixClientInfoIfNeeded(session: Session) {
diff --git a/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt
new file mode 100644
index 0000000000..386b52e61e
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.notification
+
+import im.vector.app.features.session.coroutineScope
+import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeVectorPreferences
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class NotificationsSettingUpdaterTest {
+
+    private val fakeUpdateEnableNotificationsSettingOnChangeUseCase = mockk()
+    private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk()
+
+    private val notificationsSettingUpdater = NotificationsSettingUpdater(
+            updateEnableNotificationsSettingOnChangeUseCase = fakeUpdateEnableNotificationsSettingOnChangeUseCase,
+            vectorPreferences = fakeVectorPreferences.instance,
+            updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase,
+    )
+
+    @Before
+    fun setup() {
+        mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt")
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given a session when calling onSessionStarted then update enable notification on change`() = runTest {
+        // Given
+        val aSession = FakeSession()
+        every { aSession.coroutineScope } returns this
+        coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) }
+
+        // When
+        notificationsSettingUpdater.onSessionStarted(aSession)
+        advanceUntilIdle()
+
+        // Then
+        coVerify { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(aSession) }
+    }
+
+    @Test
+    fun `given a session when calling onSessionStarted then update account data on background sync preference change`() = runTest {
+        // Given
+        val aSession = FakeSession()
+        every { aSession.coroutineScope } returns this
+        coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) }
+        coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
+        fakeVectorPreferences.givenChangeOnPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)
+
+        // When
+        notificationsSettingUpdater.onSessionStarted(aSession)
+        advanceUntilIdle()
+
+        // Then
+        coVerify { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) }
+    }
+
+    @Test
+    fun `given a session when calling onSessionStarted then account data is not updated on other preference change`() = runTest {
+        // Given
+        val aSession = FakeSession()
+        every { aSession.coroutineScope } returns this
+        coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) }
+        coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
+        fakeVectorPreferences.givenChangeOnPreference("key")
+
+        // When
+        notificationsSettingUpdater.onSessionStarted(aSession)
+        advanceUntilIdle()
+
+        // Then
+        coVerify(inverse = true) { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt
index f9f38e6c2a..2e397763f8 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt
@@ -26,6 +26,6 @@ class FakeNotificationsSettingUpdater {
     val instance = mockk()
 
     fun givenOnSessionsStarted(session: Session) {
-        justRun { instance.onSessionsStarted(session) }
+        justRun { instance.onSessionStarted(session) }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 58bc1a18b8..94bc0966c5 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -17,6 +17,7 @@
 package im.vector.app.test.fakes
 
 import im.vector.app.features.settings.BackgroundSyncMode
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import im.vector.app.features.settings.VectorPreferences
 import io.mockk.every
 import io.mockk.justRun
@@ -77,4 +78,10 @@ class FakeVectorPreferences {
     fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) {
         every { instance.isBackgroundSyncEnabled() } returns isEnabled
     }
+
+    fun givenChangeOnPreference(key: String) {
+        every { instance.subscribeToChanges(any()) } answers {
+            firstArg().onSharedPreferenceChanged(mockk(), key)
+        }
+    }
 }

From a2ae3af69d5c187930e0e0f6e4884eb3b4f29de2 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 23 Nov 2022 15:23:31 +0100
Subject: [PATCH 479/679] Removing unused imports

---
 .../vector/app/features/settings/devices/v2/DevicesViewModel.kt  | 1 -
 .../settings/devices/v2/overview/SessionOverviewViewModel.kt     | 1 -
 .../app/features/settings/devices/v2/DevicesViewModelTest.kt     | 1 -
 .../settings/devices/v2/overview/SessionOverviewViewModelTest.kt | 1 -
 4 files changed, 4 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
index bfccd2f9d3..b7a6c5df30 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
@@ -28,7 +28,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
 import im.vector.app.features.auth.PendingAuthHandler
 import im.vector.app.features.settings.VectorPreferences
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
-import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
index 74f962b464..f598c397de 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
@@ -32,7 +32,6 @@ import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCa
 import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
 import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
 import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
-import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
index aa5ebd73eb..4bfd5c4496 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt
@@ -22,7 +22,6 @@ import com.airbnb.mvrx.test.MavericksTestRule
 import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
 import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
 import im.vector.app.features.settings.devices.v2.list.DeviceType
-import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
 import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
 import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
index 2ddc91cdac..6018152176 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
@@ -24,7 +24,6 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo
 import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
 import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase
 import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus
-import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase

From 68d00e00d16b7082f6f86e2c73120387b0eb762e Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 24 Nov 2022 18:01:34 +0100
Subject: [PATCH 480/679] Fix method used to check if background sync is
 enabled

---
 ...pdateNotificationSettingsAccountDataUseCase.kt |  4 +++-
 ...eNotificationSettingsAccountDataUseCaseTest.kt | 15 +++++++++------
 .../app/test/fakes/FakeUnifiedPushHelper.kt       |  4 ++++
 3 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
index 7791c1dd4b..9296bcd912 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt
@@ -16,6 +16,7 @@
 
 package im.vector.app.features.settings.devices.v2.notification
 
+import im.vector.app.core.pushers.UnifiedPushHelper
 import im.vector.app.features.settings.VectorPreferences
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import org.matrix.android.sdk.api.session.Session
@@ -27,13 +28,14 @@ import javax.inject.Inject
  */
 class UpdateNotificationSettingsAccountDataUseCase @Inject constructor(
         private val vectorPreferences: VectorPreferences,
+        private val unifiedPushHelper: UnifiedPushHelper,
         private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase,
         private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
         private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase,
 ) {
 
     suspend fun execute(session: Session) {
-        if (vectorPreferences.isBackgroundSyncEnabled()) {
+        if (unifiedPushHelper.isBackgroundSync()) {
             setCurrentNotificationStatus(session)
         } else {
             deleteCurrentNotificationStatus(session)
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
index 3bca0da84e..f82663f6e4 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
@@ -17,6 +17,7 @@
 package im.vector.app.features.settings.devices.v2.notification
 
 import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeUnifiedPushHelper
 import im.vector.app.test.fakes.FakeVectorPreferences
 import io.mockk.coJustRun
 import io.mockk.coVerify
@@ -30,12 +31,14 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 class UpdateNotificationSettingsAccountDataUseCaseTest {
 
     private val fakeVectorPreferences = FakeVectorPreferences()
+    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
     private val fakeGetNotificationSettingsAccountDataUseCase = mockk()
     private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
     private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk()
 
     private val updateNotificationSettingsAccountDataUseCase = UpdateNotificationSettingsAccountDataUseCase(
             vectorPreferences = fakeVectorPreferences.instance,
+            unifiedPushHelper = fakeUnifiedPushHelper.instance,
             getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase,
             setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
             deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase,
@@ -50,7 +53,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val areNotificationsEnabled = true
         fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
-        fakeVectorPreferences.givenIsBackgroundSyncEnabled(true)
+        fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true)
         every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
                 LocalNotificationSettingsContent(
                         isSilenced = null
@@ -64,7 +67,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
 
         // Then
         verify {
-            fakeVectorPreferences.instance.isBackgroundSyncEnabled()
+            fakeUnifiedPushHelper.instance.isBackgroundSync()
             fakeVectorPreferences.instance.areNotificationEnabledForDevice()
             fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
         }
@@ -81,7 +84,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val areNotificationsEnabled = true
         fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
-        fakeVectorPreferences.givenIsBackgroundSyncEnabled(true)
+        fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true)
         every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
                 LocalNotificationSettingsContent(
                         isSilenced = false
@@ -95,7 +98,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
 
         // Then
         verify {
-            fakeVectorPreferences.instance.isBackgroundSyncEnabled()
+            fakeUnifiedPushHelper.instance.isBackgroundSync()
             fakeVectorPreferences.instance.areNotificationEnabledForDevice()
             fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId)
         }
@@ -110,14 +113,14 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
         val aSession = FakeSession()
         aSession.givenSessionId(aDeviceId)
         coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) }
-        fakeVectorPreferences.givenIsBackgroundSyncEnabled(false)
+        fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false)
 
         // When
         updateNotificationSettingsAccountDataUseCase.execute(aSession)
 
         // Then
         verify {
-            fakeVectorPreferences.instance.isBackgroundSyncEnabled()
+            fakeUnifiedPushHelper.instance.isBackgroundSync()
         }
         coVerify { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) }
         coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
index 99b5b75874..1a09783fad 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt
@@ -31,4 +31,8 @@ class FakeUnifiedPushHelper {
     fun givenGetEndpointOrTokenReturns(endpoint: String?) {
         every { instance.getEndpointOrToken() } returns endpoint
     }
+
+    fun givenIsBackgroundSyncReturns(enabled: Boolean) {
+        every { instance.isBackgroundSync() } returns enabled
+    }
 }

From 9dff4ff949672207112d29eab807e8f1c0a8d0d6 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 1 Dec 2022 10:30:09 +0100
Subject: [PATCH 481/679] Fixing import order after rebase

---
 .../vector/app/core/session/ConfigureAndStartSessionUseCase.kt  | 2 +-
 .../app/core/session/ConfigureAndStartSessionUseCaseTest.kt     | 2 +-
 .../test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
index d167b02d05..fbf89b76a4 100644
--- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt
@@ -24,8 +24,8 @@ import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.features.call.webrtc.WebRtcCallManager
 import im.vector.app.features.session.coroutineScope
 import im.vector.app.features.settings.VectorPreferences
-import im.vector.app.features.sync.SyncUtils
 import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
+import im.vector.app.features.sync.SyncUtils
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
 import timber.log.Timber
diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
index 4071afaf3f..3fb128c759 100644
--- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt
@@ -19,8 +19,8 @@ package im.vector.app.core.session
 import im.vector.app.core.extensions.startSyncing
 import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
 import im.vector.app.features.session.coroutineScope
-import im.vector.app.features.sync.SyncUtils
 import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
+import im.vector.app.features.sync.SyncUtils
 import im.vector.app.test.fakes.FakeContext
 import im.vector.app.test.fakes.FakeNotificationsSettingUpdater
 import im.vector.app.test.fakes.FakeSession
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 94bc0966c5..3d7de662bd 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -16,8 +16,8 @@
 
 package im.vector.app.test.fakes
 
-import im.vector.app.features.settings.BackgroundSyncMode
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import im.vector.app.features.settings.BackgroundSyncMode
 import im.vector.app.features.settings.VectorPreferences
 import io.mockk.every
 import io.mockk.justRun

From 8973f3892a3fe10df8a784f6d3042e06453430e9 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 1 Dec 2022 11:34:30 +0100
Subject: [PATCH 482/679] Fixing unit tests after rebase

---
 .../UpdateNotificationSettingsAccountDataUseCaseTest.kt       | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
index f82663f6e4..0075be02d2 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt
@@ -52,7 +52,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
         aSession.givenSessionId(aDeviceId)
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val areNotificationsEnabled = true
-        fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
+        fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled)
         fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true)
         every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
                 LocalNotificationSettingsContent(
@@ -83,7 +83,7 @@ class UpdateNotificationSettingsAccountDataUseCaseTest {
         aSession.givenSessionId(aDeviceId)
         coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
         val areNotificationsEnabled = true
-        fakeVectorPreferences.givenAreNotificationEnabled(areNotificationsEnabled)
+        fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled)
         fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true)
         every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns
                 LocalNotificationSettingsContent(

From 3f5147ddcef841caa40c56cd092916020bb06f21 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 1 Dec 2022 16:47:10 +0100
Subject: [PATCH 483/679] Fixing the toggle notifications use case for current
 session

---
 ...eCase.kt => ToggleNotificationsUseCase.kt} |  2 +-
 .../v2/overview/SessionOverviewViewModel.kt   |  6 +-
 ...leNotificationsForCurrentSessionUseCase.kt |  9 +--
 ...leNotificationsForCurrentSessionUseCase.kt | 10 +---
 ...leNotificationsForCurrentSessionUseCase.kt | 57 +++++++++++++++++++
 ...SettingsNotificationPreferenceViewModel.kt |  6 +-
 ...t.kt => ToggleNotificationsUseCaseTest.kt} | 10 ++--
 ...tificationsForCurrentSessionUseCaseTest.kt | 14 ++---
 ...tificationsForCurrentSessionUseCaseTest.kt |  8 +--
 .../fakes/FakeToggleNotificationUseCase.kt    |  4 +-
 10 files changed, 92 insertions(+), 34 deletions(-)
 rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{ToggleNotificationUseCase.kt => ToggleNotificationsUseCase.kt} (97%)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt
 rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{ToggleNotificationUseCaseTest.kt => ToggleNotificationsUseCaseTest.kt} (93%)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt
similarity index 97%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt
index 73a81a6de1..77195ea950 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt
@@ -20,7 +20,7 @@ import im.vector.app.core.di.ActiveSessionHolder
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import javax.inject.Inject
 
-class ToggleNotificationUseCase @Inject constructor(
+class ToggleNotificationsUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
         private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase,
         private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase,
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
index f598c397de..55866cb8c4 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
@@ -31,7 +31,7 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
 import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase
 import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
 import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
 import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
 import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
@@ -52,7 +52,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
         private val signoutSessionsUseCase: SignoutSessionsUseCase,
         private val pendingAuthHandler: PendingAuthHandler,
         private val activeSessionHolder: ActiveSessionHolder,
-        private val toggleNotificationUseCase: ToggleNotificationUseCase,
+        private val toggleNotificationsUseCase: ToggleNotificationsUseCase,
         private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase,
         refreshDevicesUseCase: RefreshDevicesUseCase,
         private val vectorPreferences: VectorPreferences,
@@ -226,7 +226,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
 
     private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) {
         viewModelScope.launch {
-            toggleNotificationUseCase.execute(action.deviceId, action.enabled)
+            toggleNotificationsUseCase.execute(action.deviceId, action.enabled)
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
index 380c3b5e4e..9173107018 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -20,21 +20,22 @@ import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import javax.inject.Inject
 
 class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
         private val pushersManager: PushersManager,
         private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase,
-        private val toggleNotificationUseCase: ToggleNotificationUseCase,
+        private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase,
         private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) {
 
+    // TODO update unit tests
     suspend fun execute() {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
-        val deviceId = session.sessionParams.deviceId ?: return
-        toggleNotificationUseCase.execute(deviceId, enabled = false)
+        toggleNotificationsForCurrentSessionUseCase.execute(enabled = false)
+
+        // handle case when server does not support toggle of pusher
         if (!checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) {
             unregisterUnifiedPushUseCase.execute(pushersManager)
         }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
index 89633a10c2..663c5004e8 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -16,17 +16,14 @@
 
 package im.vector.app.features.settings.notifications
 
-import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
 import javax.inject.Inject
 
 class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
-        private val activeSessionHolder: ActiveSessionHolder,
         private val pushersManager: PushersManager,
-        private val toggleNotificationUseCase: ToggleNotificationUseCase,
+        private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase,
         private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
         private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
 ) {
@@ -37,6 +34,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
         object NeedToAskUserForDistributor : EnableNotificationsResult
     }
 
+    // TODO update unit tests
     suspend fun execute(distributor: String = ""): EnableNotificationsResult {
         val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
         if (pusherForCurrentSession == null) {
@@ -50,9 +48,7 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
             }
         }
 
-        val session = activeSessionHolder.getSafeActiveSession() ?: return EnableNotificationsResult.Failure
-        val deviceId = session.sessionParams.deviceId ?: return EnableNotificationsResult.Failure
-        toggleNotificationUseCase.execute(deviceId, enabled = true)
+        toggleNotificationsForCurrentSessionUseCase.execute(enabled = true)
 
         return EnableNotificationsResult.Success
     }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt
new file mode 100644
index 0000000000..64b4b1bb89
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.notifications
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.pushers.UnifiedPushHelper
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.DeleteNotificationSettingsAccountDataUseCase
+import im.vector.app.features.settings.devices.v2.notification.SetNotificationSettingsAccountDataUseCase
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import timber.log.Timber
+import javax.inject.Inject
+
+class ToggleNotificationsForCurrentSessionUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+        private val unifiedPushHelper: UnifiedPushHelper,
+        private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase,
+        private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
+        private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase,
+) {
+
+    // TODO add unit tests
+    suspend fun execute(enabled: Boolean) {
+        val session = activeSessionHolder.getSafeActiveSession() ?: return
+        val deviceId = session.sessionParams.deviceId ?: return
+
+        if (unifiedPushHelper.isBackgroundSync()) {
+            Timber.d("background sync is enabled, setting account data event")
+            val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled)
+            setNotificationSettingsAccountDataUseCase.execute(session, deviceId, newNotificationSettingsContent)
+        } else {
+            Timber.d("push notif is enabled, deleting any account data and updating pusher")
+            deleteNotificationSettingsAccountDataUseCase.execute(session)
+
+            if (checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) {
+                val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
+                devicePusher?.let { pusher ->
+                    session.pushersService().togglePusher(pusher, enabled)
+                }
+            }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
index d6a9c621f2..357f6458f2 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
@@ -40,6 +40,7 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
         private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
         private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
         private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
+        private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase,
 ) : VectorViewModel(initialState) {
 
     @AssistedFactory
@@ -80,6 +81,7 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
         }
     }
 
+    // TODO update unit tests
     private fun handleRegisterPushDistributor(distributor: String) {
         viewModelScope.launch {
             unregisterUnifiedPushUseCase.execute(pushersManager)
@@ -88,7 +90,9 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
                     _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor)
                 }
                 RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
-                    ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
+                    val areNotificationsEnabled = vectorPreferences.areNotificationEnabledForDevice()
+                    ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = areNotificationsEnabled)
+                    toggleNotificationsForCurrentSessionUseCase.execute(enabled = areNotificationsEnabled)
                     _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged)
                 }
             }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt
similarity index 93%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt
index 1e3517c776..90afbe9045 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt
@@ -26,7 +26,7 @@ import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 
-class ToggleNotificationUseCaseTest {
+class ToggleNotificationsUseCaseTest {
 
     private val activeSessionHolder = FakeActiveSessionHolder()
     private val fakeCheckIfCanToggleNotificationsViaPusherUseCase =
@@ -36,8 +36,8 @@ class ToggleNotificationUseCaseTest {
     private val fakeSetNotificationSettingsAccountDataUseCase =
             mockk()
 
-    private val toggleNotificationUseCase =
-            ToggleNotificationUseCase(
+    private val toggleNotificationsUseCase =
+            ToggleNotificationsUseCase(
                     activeSessionHolder = activeSessionHolder.instance,
                     checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
                     checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
@@ -59,7 +59,7 @@ class ToggleNotificationUseCaseTest {
         every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false
 
         // When
-        toggleNotificationUseCase.execute(sessionId, true)
+        toggleNotificationsUseCase.execute(sessionId, true)
 
         // Then
         activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true)
@@ -78,7 +78,7 @@ class ToggleNotificationUseCaseTest {
         )
 
         // When
-        toggleNotificationUseCase.execute(sessionId, true)
+        toggleNotificationsUseCase.execute(sessionId, true)
 
         // Then
         coVerify {
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
index e53874858a..7e2a118e0e 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -18,7 +18,7 @@ package im.vector.app.features.settings.notifications
 
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
 import io.mockk.coJustRun
@@ -35,14 +35,14 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakePushersManager = FakePushersManager()
     private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk()
-    private val fakeToggleNotificationUseCase = mockk()
+    private val fakeToggleNotificationsUseCase = mockk()
     private val fakeUnregisterUnifiedPushUseCase = mockk()
 
     private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
             activeSessionHolder = fakeActiveSessionHolder.instance,
             pushersManager = fakePushersManager.instance,
             checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
-            toggleNotificationUseCase = fakeToggleNotificationUseCase,
+            toggleNotificationUseCase = fakeToggleNotificationsUseCase,
             unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
     )
 
@@ -52,13 +52,13 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
         val fakeSession = fakeActiveSessionHolder.fakeSession
         fakeSession.givenSessionId(A_SESSION_ID)
         every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true
-        coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) }
+        coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) }
 
         // When
         disableNotificationsForCurrentSessionUseCase.execute()
 
         // Then
-        coVerify { fakeToggleNotificationUseCase.execute(A_SESSION_ID, false) }
+        coVerify { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false) }
     }
 
     @Test
@@ -67,7 +67,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
         val fakeSession = fakeActiveSessionHolder.fakeSession
         fakeSession.givenSessionId(A_SESSION_ID)
         every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false
-        coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) }
+        coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) }
         coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
 
         // When
@@ -75,7 +75,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
 
         // Then
         coVerify {
-            fakeToggleNotificationUseCase.execute(A_SESSION_ID, false)
+            fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false)
             fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
         }
     }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
index f10b0777cb..7beab170f2 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
@@ -18,7 +18,7 @@ package im.vector.app.features.settings.notifications
 
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
 import io.mockk.coJustRun
@@ -36,14 +36,14 @@ class EnableNotificationsForCurrentSessionUseCaseTest {
 
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakePushersManager = FakePushersManager()
-    private val fakeToggleNotificationUseCase = mockk()
+    private val fakeToggleNotificationsUseCase = mockk()
     private val fakeRegisterUnifiedPushUseCase = mockk()
     private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk()
 
     private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase(
             activeSessionHolder = fakeActiveSessionHolder.instance,
             pushersManager = fakePushersManager.instance,
-            toggleNotificationUseCase = fakeToggleNotificationUseCase,
+            toggleNotificationUseCase = fakeToggleNotificationsUseCase,
             registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
             ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
     )
@@ -57,7 +57,7 @@ class EnableNotificationsForCurrentSessionUseCaseTest {
         fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
         every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
         justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
-        coJustRun { fakeToggleNotificationUseCase.execute(A_SESSION_ID, any()) }
+        coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) }
 
         // When
         val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt
index 527625144e..3d2179bc2d 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt
@@ -16,14 +16,14 @@
 
 package im.vector.app.test.fakes
 
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationUseCase
+import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase
 import io.mockk.coJustRun
 import io.mockk.coVerify
 import io.mockk.mockk
 
 class FakeToggleNotificationUseCase {
 
-    val instance = mockk {
+    val instance = mockk {
         coJustRun { execute(any(), any()) }
     }
 

From 06681fd115596ee9b14a30aef94f73d3a62de79b Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 1 Dec 2022 17:12:09 +0100
Subject: [PATCH 484/679] Removing listening on background sync preference

---
 .../NotificationsSettingUpdater.kt            | 27 ------------
 .../NotificationsSettingUpdaterTest.kt        | 41 -------------------
 2 files changed, 68 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
index 4a16f37cfe..a4d18baa64 100644
--- a/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
+++ b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt
@@ -16,11 +16,7 @@
 
 package im.vector.app.core.notification
 
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import im.vector.app.features.session.coroutineScope
-import im.vector.app.features.settings.VectorPreferences
-import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_FDROID_BACKGROUND_SYNC_MODE
-import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
@@ -29,21 +25,16 @@ import javax.inject.Singleton
 
 /**
  * Listen changes in Pusher or Account Data to update the local setting for notification toggle.
- * Listen changes on background sync mode preference to update the corresponding Account Data event.
  */
 @Singleton
 class NotificationsSettingUpdater @Inject constructor(
         private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase,
-        private val vectorPreferences: VectorPreferences,
-        private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase,
 ) {
 
     private var job: Job? = null
-    private var prefChangeListener: OnSharedPreferenceChangeListener? = null
 
     fun onSessionStarted(session: Session) {
         updateEnableNotificationsSettingOnChange(session)
-        updateAccountDataOnBackgroundSyncChange(session)
     }
 
     private fun updateEnableNotificationsSettingOnChange(session: Session) {
@@ -52,22 +43,4 @@ class NotificationsSettingUpdater @Inject constructor(
             updateEnableNotificationsSettingOnChangeUseCase.execute(session)
         }
     }
-
-    private fun updateAccountDataOnBackgroundSyncChange(session: Session) {
-        prefChangeListener?.let { vectorPreferences.unsubscribeToChanges(it) }
-        prefChangeListener = null
-        prefChangeListener = createPrefListener(session).also {
-            vectorPreferences.subscribeToChanges(it)
-        }
-    }
-
-    private fun createPrefListener(session: Session): OnSharedPreferenceChangeListener {
-        return OnSharedPreferenceChangeListener { _, key ->
-            session.coroutineScope.launch {
-                if (key == SETTINGS_FDROID_BACKGROUND_SYNC_MODE) {
-                    updateNotificationSettingsAccountDataUseCase.execute(session)
-                }
-            }
-        }
-    }
 }
diff --git a/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt
index 386b52e61e..0920ee4716 100644
--- a/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt
+++ b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt
@@ -17,10 +17,7 @@
 package im.vector.app.core.notification
 
 import im.vector.app.features.session.coroutineScope
-import im.vector.app.features.settings.VectorPreferences
-import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
 import im.vector.app.test.fakes.FakeSession
-import im.vector.app.test.fakes.FakeVectorPreferences
 import io.mockk.coJustRun
 import io.mockk.coVerify
 import io.mockk.every
@@ -36,13 +33,9 @@ import org.junit.Test
 class NotificationsSettingUpdaterTest {
 
     private val fakeUpdateEnableNotificationsSettingOnChangeUseCase = mockk()
-    private val fakeVectorPreferences = FakeVectorPreferences()
-    private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk()
 
     private val notificationsSettingUpdater = NotificationsSettingUpdater(
             updateEnableNotificationsSettingOnChangeUseCase = fakeUpdateEnableNotificationsSettingOnChangeUseCase,
-            vectorPreferences = fakeVectorPreferences.instance,
-            updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase,
     )
 
     @Before
@@ -69,38 +62,4 @@ class NotificationsSettingUpdaterTest {
         // Then
         coVerify { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(aSession) }
     }
-
-    @Test
-    fun `given a session when calling onSessionStarted then update account data on background sync preference change`() = runTest {
-        // Given
-        val aSession = FakeSession()
-        every { aSession.coroutineScope } returns this
-        coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) }
-        coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
-        fakeVectorPreferences.givenChangeOnPreference(VectorPreferences.SETTINGS_FDROID_BACKGROUND_SYNC_MODE)
-
-        // When
-        notificationsSettingUpdater.onSessionStarted(aSession)
-        advanceUntilIdle()
-
-        // Then
-        coVerify { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) }
-    }
-
-    @Test
-    fun `given a session when calling onSessionStarted then account data is not updated on other preference change`() = runTest {
-        // Given
-        val aSession = FakeSession()
-        every { aSession.coroutineScope } returns this
-        coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) }
-        coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) }
-        fakeVectorPreferences.givenChangeOnPreference("key")
-
-        // When
-        notificationsSettingUpdater.onSessionStarted(aSession)
-        advanceUntilIdle()
-
-        // Then
-        coVerify(inverse = true) { fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) }
-    }
 }

From 5248a69fe2bd498a892f5484fff96b60ccec0154 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 1 Dec 2022 17:33:27 +0100
Subject: [PATCH 485/679] Updating existing unit tests

---
 ...leNotificationsForCurrentSessionUseCase.kt |  1 -
 ...leNotificationsForCurrentSessionUseCase.kt |  2 --
 ...rSettingsNotificationPreferenceFragment.kt |  1 -
 ...SettingsNotificationPreferenceViewEvent.kt |  1 -
 ...SettingsNotificationPreferenceViewModel.kt |  4 ---
 .../overview/SessionOverviewViewModelTest.kt  |  2 +-
 ...tificationsForCurrentSessionUseCaseTest.kt | 20 +++++------
 ...tificationsForCurrentSessionUseCaseTest.kt | 34 ++++---------------
 ...ingsNotificationPreferenceViewModelTest.kt | 27 +++------------
 9 files changed, 21 insertions(+), 71 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
index 9173107018..daa58578d6 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -30,7 +30,6 @@ class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
         private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) {
 
-    // TODO update unit tests
     suspend fun execute() {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
         toggleNotificationsForCurrentSessionUseCase.execute(enabled = false)
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
index 663c5004e8..daf3890e33 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt
@@ -30,11 +30,9 @@ class EnableNotificationsForCurrentSessionUseCase @Inject constructor(
 
     sealed interface EnableNotificationsResult {
         object Success : EnableNotificationsResult
-        object Failure : EnableNotificationsResult
         object NeedToAskUserForDistributor : EnableNotificationsResult
     }
 
-    // TODO update unit tests
     suspend fun execute(distributor: String = ""): EnableNotificationsResult {
         val pusherForCurrentSession = pushersManager.getPusherForCurrentSession()
         if (pusherForCurrentSession == null) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
index 238ed4218c..490a47ef61 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt
@@ -119,7 +119,6 @@ class VectorSettingsNotificationPreferenceFragment :
                 VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled -> onNotificationsForDeviceEnabled()
                 VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled -> onNotificationsForDeviceDisabled()
                 is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor()
-                VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure -> displayErrorDialog(throwable = null)
                 VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged -> onNotificationMethodChanged()
             }
         }
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
index e4cf8e1973..b0ee107769 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt
@@ -20,7 +20,6 @@ import im.vector.app.core.platform.VectorViewEvents
 
 sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents {
     object NotificationsForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent
-    object EnableNotificationForDeviceFailure : VectorSettingsNotificationPreferenceViewEvent
     object NotificationsForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent
     object AskUserForPushDistributor : VectorSettingsNotificationPreferenceViewEvent
     object NotificationMethodChanged : VectorSettingsNotificationPreferenceViewEvent
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
index 357f6458f2..48e82b35e8 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
@@ -68,9 +68,6 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
     private fun handleEnableNotificationsForDevice(distributor: String) {
         viewModelScope.launch {
             when (enableNotificationsForCurrentSessionUseCase.execute(distributor)) {
-                EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure -> {
-                    _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure)
-                }
                 is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> {
                     _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor)
                 }
@@ -81,7 +78,6 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
         }
     }
 
-    // TODO update unit tests
     private fun handleRegisterPushDistributor(distributor: String) {
         viewModelScope.launch {
             unregisterUnifiedPushUseCase.execute(pushersManager)
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
index 6018152176..b0f7a774f2 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
@@ -88,7 +88,7 @@ class SessionOverviewViewModelTest {
             pendingAuthHandler = fakePendingAuthHandler.instance,
             activeSessionHolder = fakeActiveSessionHolder.instance,
             refreshDevicesUseCase = refreshDevicesUseCase,
-            toggleNotificationUseCase = toggleNotificationUseCase.instance,
+            toggleNotificationsUseCase = toggleNotificationUseCase.instance,
             getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance,
             vectorPreferences = fakeVectorPreferences.instance,
             toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase,
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
index 7e2a118e0e..b7749d0252 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -18,7 +18,6 @@ package im.vector.app.features.settings.notifications
 
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
 import io.mockk.coJustRun
@@ -28,21 +27,19 @@ import io.mockk.mockk
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 
-private const val A_SESSION_ID = "session-id"
-
 class DisableNotificationsForCurrentSessionUseCaseTest {
 
     private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakePushersManager = FakePushersManager()
     private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk()
-    private val fakeToggleNotificationsUseCase = mockk()
+    private val fakeToggleNotificationsForCurrentSessionUseCase = mockk()
     private val fakeUnregisterUnifiedPushUseCase = mockk()
 
     private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
             activeSessionHolder = fakeActiveSessionHolder.instance,
             pushersManager = fakePushersManager.instance,
             checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
-            toggleNotificationUseCase = fakeToggleNotificationsUseCase,
+            toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase,
             unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
     )
 
@@ -50,24 +47,25 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
     fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest {
         // Given
         val fakeSession = fakeActiveSessionHolder.fakeSession
-        fakeSession.givenSessionId(A_SESSION_ID)
         every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true
-        coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) }
+        coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) }
 
         // When
         disableNotificationsForCurrentSessionUseCase.execute()
 
         // Then
-        coVerify { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false) }
+        coVerify { fakeToggleNotificationsForCurrentSessionUseCase.execute(false) }
+        coVerify(inverse = true) {
+            fakeUnregisterUnifiedPushUseCase.execute(any())
+        }
     }
 
     @Test
     fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest {
         // Given
         val fakeSession = fakeActiveSessionHolder.fakeSession
-        fakeSession.givenSessionId(A_SESSION_ID)
         every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false
-        coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) }
+        coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) }
         coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
 
         // When
@@ -75,7 +73,7 @@ class DisableNotificationsForCurrentSessionUseCaseTest {
 
         // Then
         coVerify {
-            fakeToggleNotificationsUseCase.execute(A_SESSION_ID, false)
+            fakeToggleNotificationsForCurrentSessionUseCase.execute(false)
             fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
         }
     }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
index 7beab170f2..d58ba7645c 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt
@@ -18,10 +18,9 @@ package im.vector.app.features.settings.notifications
 
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase
-import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
 import io.mockk.coJustRun
+import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.justRun
 import io.mockk.mockk
@@ -30,20 +29,16 @@ import kotlinx.coroutines.test.runTest
 import org.amshove.kluent.shouldBe
 import org.junit.Test
 
-private const val A_SESSION_ID = "session-id"
-
 class EnableNotificationsForCurrentSessionUseCaseTest {
 
-    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakePushersManager = FakePushersManager()
-    private val fakeToggleNotificationsUseCase = mockk()
+    private val fakeToggleNotificationsForCurrentSessionUseCase = mockk()
     private val fakeRegisterUnifiedPushUseCase = mockk()
     private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk()
 
     private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase(
-            activeSessionHolder = fakeActiveSessionHolder.instance,
             pushersManager = fakePushersManager.instance,
-            toggleNotificationUseCase = fakeToggleNotificationsUseCase,
+            toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase,
             registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
             ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
     )
@@ -52,12 +47,10 @@ class EnableNotificationsForCurrentSessionUseCaseTest {
     fun `given no existing pusher and a registered distributor when execute then a new pusher is registered and result is success`() = runTest {
         // Given
         val aDistributor = "distributor"
-        val fakeSession = fakeActiveSessionHolder.fakeSession
-        fakeSession.givenSessionId(A_SESSION_ID)
         fakePushersManager.givenGetPusherForCurrentSessionReturns(null)
         every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success
         justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
-        coJustRun { fakeToggleNotificationsUseCase.execute(A_SESSION_ID, any()) }
+        coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) }
 
         // When
         val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
@@ -68,6 +61,9 @@ class EnableNotificationsForCurrentSessionUseCaseTest {
             fakeRegisterUnifiedPushUseCase.execute(aDistributor)
             fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = true)
         }
+        coVerify {
+            fakeToggleNotificationsForCurrentSessionUseCase.execute(enabled = true)
+        }
     }
 
     @Test
@@ -86,20 +82,4 @@ class EnableNotificationsForCurrentSessionUseCaseTest {
             fakeRegisterUnifiedPushUseCase.execute(aDistributor)
         }
     }
-
-    @Test
-    fun `given no deviceId for current session when execute then result is failure`() = runTest {
-        // Given
-        val aDistributor = "distributor"
-        val fakeSession = fakeActiveSessionHolder.fakeSession
-        fakeSession.givenSessionId(null)
-        fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk())
-        every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor
-
-        // When
-        val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor)
-
-        // Then
-        result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure
-    }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
index f9d7527316..270447c461 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
@@ -46,6 +46,7 @@ class VectorSettingsNotificationPreferenceViewModelTest {
     private val fakeUnregisterUnifiedPushUseCase = mockk()
     private val fakeRegisterUnifiedPushUseCase = mockk()
     private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk()
+    private val fakeToggleNotificationsForCurrentSessionUseCase = mockk()
 
     private fun createViewModel() = VectorSettingsNotificationPreferenceViewModel(
             initialState = VectorDummyViewState(),
@@ -56,6 +57,7 @@ class VectorSettingsNotificationPreferenceViewModelTest {
             unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
             registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase,
             ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase,
+            toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase,
     )
 
     @Test
@@ -125,29 +127,6 @@ class VectorSettingsNotificationPreferenceViewModelTest {
         }
     }
 
-    @Test
-    fun `given EnableNotificationsForDevice action and enable failure when handling action then enable use case is called`() {
-        // Given
-        val viewModel = createViewModel()
-        val aDistributor = "aDistributor"
-        val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor)
-        coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns
-                EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Failure
-        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.EnableNotificationForDeviceFailure
-
-        // When
-        val viewModelTest = viewModel.test()
-        viewModel.handle(action)
-
-        // Then
-        viewModelTest
-                .assertEvent { event -> event == expectedEvent }
-                .finish()
-        coVerify {
-            fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor)
-        }
-    }
-
     @Test
     fun `given RegisterPushDistributor action and register success when handling action then register use case is called`() {
         // Given
@@ -158,6 +137,7 @@ class VectorSettingsNotificationPreferenceViewModelTest {
         coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
         val areNotificationsEnabled = true
         fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled)
+        coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) }
         justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) }
         val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged
 
@@ -173,6 +153,7 @@ class VectorSettingsNotificationPreferenceViewModelTest {
             fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance)
             fakeRegisterUnifiedPushUseCase.execute(aDistributor)
             fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = areNotificationsEnabled)
+            fakeToggleNotificationsForCurrentSessionUseCase.execute(enabled = areNotificationsEnabled)
         }
     }
 

From b78de152286cc75f350a444863e6625d79144752 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 1 Dec 2022 17:55:30 +0100
Subject: [PATCH 486/679] Adding unit tests for new toggle notification for
 current session use case

---
 ...leNotificationsForCurrentSessionUseCase.kt |   1 -
 ...tificationsForCurrentSessionUseCaseTest.kt | 117 ++++++++++++++++++
 2 files changed, 117 insertions(+), 1 deletion(-)
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt

diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt
index 64b4b1bb89..3dc73f0a31 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt
@@ -33,7 +33,6 @@ class ToggleNotificationsForCurrentSessionUseCase @Inject constructor(
         private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase,
 ) {
 
-    // TODO add unit tests
     suspend fun execute(enabled: Boolean) {
         val session = activeSessionHolder.getSafeActiveSession() ?: return
         val deviceId = session.sessionParams.deviceId ?: return
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt
new file mode 100644
index 0000000000..f49aafab8a
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.notifications
+
+import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
+import im.vector.app.features.settings.devices.v2.notification.DeleteNotificationSettingsAccountDataUseCase
+import im.vector.app.features.settings.devices.v2.notification.SetNotificationSettingsAccountDataUseCase
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeUnifiedPushHelper
+import im.vector.app.test.fixtures.PusherFixture
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+
+class ToggleNotificationsForCurrentSessionUseCaseTest {
+
+    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+    private val fakeUnifiedPushHelper = FakeUnifiedPushHelper()
+    private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk()
+    private val fakeSetNotificationSettingsAccountDataUseCase = mockk()
+    private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk()
+
+    private val toggleNotificationsForCurrentSessionUseCase = ToggleNotificationsForCurrentSessionUseCase(
+            activeSessionHolder = fakeActiveSessionHolder.instance,
+            unifiedPushHelper = fakeUnifiedPushHelper.instance,
+            checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
+            setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase,
+            deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase,
+    )
+
+    @Test
+    fun `given background sync is enabled when execute then set the related account data with correct value`() = runTest {
+        // Given
+        val enabled = true
+        val aDeviceId = "deviceId"
+        fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true)
+        fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId)
+        coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) }
+        val expectedNotificationContent = LocalNotificationSettingsContent(isSilenced = !enabled)
+
+        // When
+        toggleNotificationsForCurrentSessionUseCase.execute(enabled)
+
+        // Then
+        coVerify {
+            fakeSetNotificationSettingsAccountDataUseCase.execute(
+                    fakeActiveSessionHolder.fakeSession,
+                    aDeviceId,
+                    expectedNotificationContent
+            )
+        }
+    }
+
+    @Test
+    fun `given background sync is not enabled and toggle pusher is possible when execute then delete any related account data and toggle pusher`() = runTest {
+        // Given
+        val enabled = true
+        val aDeviceId = "deviceId"
+        fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false)
+        fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId)
+        coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) }
+        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(any()) } returns true
+        val aPusher = PusherFixture.aPusher(deviceId = aDeviceId)
+        fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(aPusher))
+
+        // When
+        toggleNotificationsForCurrentSessionUseCase.execute(enabled)
+
+        // Then
+        coVerify {
+            fakeDeleteNotificationSettingsAccountDataUseCase.execute(fakeActiveSessionHolder.fakeSession)
+            fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession)
+        }
+        fakeActiveSessionHolder.fakeSession.fakePushersService.verifyTogglePusherCalled(aPusher, enabled)
+    }
+
+    @Test
+    fun `given background sync is not enabled and toggle pusher is not possible when execute then only delete any related account data`() = runTest {
+        // Given
+        val enabled = true
+        val aDeviceId = "deviceId"
+        fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false)
+        fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId)
+        coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) }
+        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(any()) } returns false
+
+        // When
+        toggleNotificationsForCurrentSessionUseCase.execute(enabled)
+
+        // Then
+        coVerify {
+            fakeDeleteNotificationSettingsAccountDataUseCase.execute(fakeActiveSessionHolder.fakeSession)
+            fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession)
+        }
+        coVerify(inverse = true) {
+            fakeActiveSessionHolder.fakeSession.fakePushersService.togglePusher(any(), any())
+        }
+    }
+}

From e09b9a2ce0e245e1311374135807c835bf14e419 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 10:03:37 +0100
Subject: [PATCH 487/679] Fixing wrong notification status when no registered
 pusher for the session

---
 .../notification/GetNotificationsStatusUseCase.kt  |  8 +++++++-
 .../GetNotificationsStatusUseCaseTest.kt           | 14 ++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
index f98fd63efb..ae7e859573 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
@@ -51,7 +51,13 @@ class GetNotificationsStatusUseCase @Inject constructor(
                                     .livePushers()
                                     .map { it.filter { pusher -> pusher.deviceId == deviceId } }
                                     .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
-                                    .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
+                                    .map {
+                                        when (it) {
+                                            true -> NotificationsStatus.ENABLED
+                                            false -> NotificationsStatus.DISABLED
+                                            else -> NotificationsStatus.NOT_SUPPORTED
+                                        }
+                                    }
                                     .distinctUntilChanged()
                         } else {
                             flowOf(NotificationsStatus.NOT_SUPPORTED)
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
index d4c3aa5788..3c454f7965 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
@@ -105,6 +105,20 @@ class GetNotificationsStatusUseCaseTest {
         result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
     }
 
+    @Test
+    fun `given toggle via pusher is supported and no registered pusher when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
+        // Given
+        fakeSession.pushersService().givenPushersLive(emptyList())
+        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
+
+        // When
+        val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID)
+
+        // Then
+        result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
+    }
+
     @Test
     fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest {
         // Given

From 635f975b6cb5bbb8170a982e4b713e432861e9d4 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 15:13:10 +0100
Subject: [PATCH 488/679] Fix missing unregister of pusher when notifications
 are disabled

---
 .../features/home/HomeActivityViewModel.kt    | 10 +++++++
 ...leNotificationsForCurrentSessionUseCase.kt | 11 +-------
 ...tificationsForCurrentSessionUseCaseTest.kt | 28 +------------------
 3 files changed, 12 insertions(+), 37 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
index 26034fc09c..a54ce2cff3 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt
@@ -29,6 +29,7 @@ import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
+import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
 import im.vector.app.features.analytics.AnalyticsConfig
 import im.vector.app.features.analytics.AnalyticsTracker
 import im.vector.app.features.analytics.extensions.toAnalyticsType
@@ -92,6 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor(
         private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
         private val pushersManager: PushersManager,
         private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
+        private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
         private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
 ) : VectorViewModel(initialState) {
 
@@ -130,6 +132,8 @@ class HomeActivityViewModel @AssistedInject constructor(
     private fun registerUnifiedPushIfNeeded() {
         if (vectorPreferences.areNotificationEnabledForDevice()) {
             registerUnifiedPush(distributor = "")
+        } else {
+            unregisterUnifiedPush()
         }
     }
 
@@ -146,6 +150,12 @@ class HomeActivityViewModel @AssistedInject constructor(
         }
     }
 
+    private fun unregisterUnifiedPush() {
+        viewModelScope.launch {
+            unregisterUnifiedPushUseCase.execute(pushersManager)
+        }
+    }
+
     private fun observeReleaseNotes() = withState { state ->
         if (vectorPreferences.isNewAppLayoutEnabled()) {
             // we don't want to show release notes for new users or after relogin
diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
index daa58578d6..0c50a296f3 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt
@@ -16,27 +16,18 @@
 
 package im.vector.app.features.settings.notifications
 
-import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.pushers.PushersManager
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
 import javax.inject.Inject
 
 class DisableNotificationsForCurrentSessionUseCase @Inject constructor(
-        private val activeSessionHolder: ActiveSessionHolder,
         private val pushersManager: PushersManager,
-        private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase,
         private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase,
         private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
 ) {
 
     suspend fun execute() {
-        val session = activeSessionHolder.getSafeActiveSession() ?: return
         toggleNotificationsForCurrentSessionUseCase.execute(enabled = false)
-
-        // handle case when server does not support toggle of pusher
-        if (!checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) {
-            unregisterUnifiedPushUseCase.execute(pushersManager)
-        }
+        unregisterUnifiedPushUseCase.execute(pushersManager)
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
index b7749d0252..669b20fc1a 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt
@@ -17,54 +17,28 @@
 package im.vector.app.features.settings.notifications
 
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
-import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase
-import im.vector.app.test.fakes.FakeActiveSessionHolder
 import im.vector.app.test.fakes.FakePushersManager
 import io.mockk.coJustRun
 import io.mockk.coVerify
-import io.mockk.every
 import io.mockk.mockk
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 
 class DisableNotificationsForCurrentSessionUseCaseTest {
 
-    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
     private val fakePushersManager = FakePushersManager()
-    private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk()
     private val fakeToggleNotificationsForCurrentSessionUseCase = mockk()
     private val fakeUnregisterUnifiedPushUseCase = mockk()
 
     private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase(
-            activeSessionHolder = fakeActiveSessionHolder.instance,
             pushersManager = fakePushersManager.instance,
-            checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase,
             toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase,
             unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase,
     )
 
     @Test
-    fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest {
+    fun `when execute then disable notifications and unregister the pusher`() = runTest {
         // Given
-        val fakeSession = fakeActiveSessionHolder.fakeSession
-        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true
-        coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) }
-
-        // When
-        disableNotificationsForCurrentSessionUseCase.execute()
-
-        // Then
-        coVerify { fakeToggleNotificationsForCurrentSessionUseCase.execute(false) }
-        coVerify(inverse = true) {
-            fakeUnregisterUnifiedPushUseCase.execute(any())
-        }
-    }
-
-    @Test
-    fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest {
-        // Given
-        val fakeSession = fakeActiveSessionHolder.fakeSession
-        every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false
         coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) }
         coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) }
 

From 18ab8a1279c94058b55d127df3e6985c2b5014fa Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 23 Nov 2022 17:22:34 +0100
Subject: [PATCH 489/679] Adding changelog entry

---
 changelog.d/7632.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7632.feature

diff --git a/changelog.d/7632.feature b/changelog.d/7632.feature
new file mode 100644
index 0000000000..460f987756
--- /dev/null
+++ b/changelog.d/7632.feature
@@ -0,0 +1 @@
+Update notifications setting when m.local_notification_settings. event changes for current device

From 9fbfe82044ec6792a114856a0bf408d40123f290 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 24 Nov 2022 16:56:06 +0100
Subject: [PATCH 490/679] Fix observation of the notification status for the
 current session

---
 ...oggleNotificationsViaAccountDataUseCase.kt |  32 +++++
 ...icationSettingsAccountDataAsFlowUseCase.kt |  37 ++++++
 .../GetNotificationsStatusUseCase.kt          |  34 +++---
 ...eNotificationsViaAccountDataUseCaseTest.kt |  88 ++++++++++++++
 ...ionSettingsAccountDataAsFlowUseCaseTest.kt | 109 ++++++++++++++++++
 .../GetNotificationsStatusUseCaseTest.kt      |  25 ++--
 .../fakes/FakeSessionAccountDataService.kt    |  11 ++
 7 files changed, 314 insertions(+), 22 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt
new file mode 100644
index 0000000000..ac466852eb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import org.matrix.android.sdk.api.session.Session
+import javax.inject.Inject
+
+class CanToggleNotificationsViaAccountDataUseCase @Inject constructor(
+        private val getNotificationSettingsAccountDataAsFlowUseCase: GetNotificationSettingsAccountDataAsFlowUseCase,
+) {
+
+    fun execute(session: Session, deviceId: String): Flow {
+        return getNotificationSettingsAccountDataAsFlowUseCase.execute(session, deviceId)
+                .map { it?.isSilenced != null }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt
new file mode 100644
index 0000000000..ea4bd40f1f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import androidx.lifecycle.asFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toModel
+import javax.inject.Inject
+
+class GetNotificationSettingsAccountDataAsFlowUseCase @Inject constructor() {
+
+    fun execute(session: Session, deviceId: String): Flow {
+        return session
+                .accountDataService()
+                .getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
+                .asFlow()
+                .map { it.getOrNull()?.content?.toModel() }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
index ae7e859573..8cf684975e 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt
@@ -31,20 +31,30 @@ import javax.inject.Inject
 
 class GetNotificationsStatusUseCase @Inject constructor(
         private val canToggleNotificationsViaPusherUseCase: CanToggleNotificationsViaPusherUseCase,
-        private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase,
+        private val canToggleNotificationsViaAccountDataUseCase: CanToggleNotificationsViaAccountDataUseCase,
 ) {
 
     fun execute(session: Session, deviceId: String): Flow {
-        return when {
-            checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId) -> {
-                session.flow()
-                        .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
-                        .unwrap()
-                        .map { it.content.toModel()?.isSilenced?.not() }
-                        .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
-                        .distinctUntilChanged()
-            }
-            else -> canToggleNotificationsViaPusherUseCase.execute(session)
+        return canToggleNotificationsViaAccountDataUseCase.execute(session, deviceId)
+                .flatMapLatest { canToggle ->
+                    if (canToggle) {
+                        notificationStatusFromAccountData(session, deviceId)
+                    } else {
+                        notificationStatusFromPusher(session, deviceId)
+                    }
+                }
+                .distinctUntilChanged()
+    }
+
+    private fun notificationStatusFromAccountData(session: Session, deviceId: String) =
+            session.flow()
+                    .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
+                    .unwrap()
+                    .map { it.content.toModel()?.isSilenced?.not() }
+                    .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
+
+    private fun notificationStatusFromPusher(session: Session, deviceId: String) =
+            canToggleNotificationsViaPusherUseCase.execute(session)
                     .flatMapLatest { canToggle ->
                         if (canToggle) {
                             session.flow()
@@ -63,6 +73,4 @@ class GetNotificationsStatusUseCase @Inject constructor(
                             flowOf(NotificationsStatus.NOT_SUPPORTED)
                         }
                     }
-        }
-    }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt
new file mode 100644
index 0000000000..a1dfed6902
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeSession
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBe
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+
+class CanToggleNotificationsViaAccountDataUseCaseTest {
+
+    private val fakeGetNotificationSettingsAccountDataAsFlowUseCase = mockk()
+
+    private val canToggleNotificationsViaAccountDataUseCase = CanToggleNotificationsViaAccountDataUseCase(
+            getNotificationSettingsAccountDataAsFlowUseCase = fakeGetNotificationSettingsAccountDataAsFlowUseCase,
+    )
+
+    @Test
+    fun `given current session and content for account data when execute then true is returned`() = runTest {
+        // Given
+        val aSession = FakeSession()
+        val aDeviceId = "aDeviceId"
+        val localNotificationSettingsContent = LocalNotificationSettingsContent(
+                isSilenced = true,
+        )
+        every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent)
+
+        // When
+        val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull()
+
+        // Then
+        result shouldBe true
+        verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) }
+    }
+
+    @Test
+    fun `given current session and empty content for account data when execute then false is returned`() = runTest {
+        // Given
+        val aSession = FakeSession()
+        val aDeviceId = "aDeviceId"
+        val localNotificationSettingsContent = LocalNotificationSettingsContent(
+                isSilenced = null,
+        )
+        every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent)
+
+        // When
+        val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull()
+
+        // Then
+        result shouldBe false
+        verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) }
+    }
+
+    @Test
+    fun `given current session and no related account data when execute then false is returned`() = runTest {
+        // Given
+        val aSession = FakeSession()
+        val aDeviceId = "aDeviceId"
+        every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(null)
+
+        // When
+        val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull()
+
+        // Then
+        result shouldBe false
+        verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt
new file mode 100644
index 0000000000..6280d4c48b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2.notification
+
+import im.vector.app.test.fakes.FakeFlowLiveDataConversions
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.givenAsFlow
+import io.mockk.unmockkAll
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
+import org.matrix.android.sdk.api.session.events.model.toContent
+
+class GetNotificationSettingsAccountDataAsFlowUseCaseTest {
+
+    private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
+    private val getNotificationSettingsAccountDataAsFlowUseCase = GetNotificationSettingsAccountDataAsFlowUseCase()
+
+    @Before
+    fun setUp() {
+        fakeFlowLiveDataConversions.setup()
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given a device id when execute then retrieve the account data event corresponding to this id if any`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        val expectedContent = LocalNotificationSettingsContent(isSilenced = true)
+        aSession
+                .accountDataService()
+                .givenGetLiveUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId,
+                        content = expectedContent.toContent(),
+                )
+                .givenAsFlow()
+
+        // When
+        val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull()
+
+        // Then
+        result shouldBeEqualTo expectedContent
+    }
+
+    @Test
+    fun `given a device id and no content for account data when execute then retrieve the account data event corresponding to this id if any`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        aSession
+                .accountDataService()
+                .givenGetLiveUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId,
+                        content = null,
+                )
+                .givenAsFlow()
+
+        // When
+        val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull()
+
+        // Then
+        result shouldBeEqualTo null
+    }
+
+    @Test
+    fun `given a device id and empty content for account data when execute then retrieve the account data event corresponding to this id if any`() = runTest {
+        // Given
+        val aDeviceId = "device-id"
+        val aSession = FakeSession()
+        val expectedContent = LocalNotificationSettingsContent(isSilenced = null)
+        aSession
+                .accountDataService()
+                .givenGetLiveUserAccountDataEventReturns(
+                        type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId,
+                        content = expectedContent.toContent(),
+                )
+                .givenAsFlow()
+
+        // When
+        val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull()
+
+        // Then
+        result shouldBeEqualTo expectedContent
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
index 3c454f7965..e4b681c5ec 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt
@@ -22,6 +22,7 @@ import im.vector.app.test.fixtures.PusherFixture
 import im.vector.app.test.testDispatcher
 import io.mockk.every
 import io.mockk.mockk
+import io.mockk.verify
 import io.mockk.verifyOrder
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.firstOrNull
@@ -46,14 +47,14 @@ class GetNotificationsStatusUseCaseTest {
     val instantTaskExecutorRule = InstantTaskExecutorRule()
 
     private val fakeSession = FakeSession()
-    private val fakeCheckIfCanToggleNotificationsViaAccountDataUseCase =
-            mockk()
+    private val fakeCanToggleNotificationsViaAccountDataUseCase =
+            mockk()
     private val fakeCanToggleNotificationsViaPusherUseCase =
             mockk()
 
     private val getNotificationsStatusUseCase =
             GetNotificationsStatusUseCase(
-                    checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase,
+                    canToggleNotificationsViaAccountDataUseCase = fakeCanToggleNotificationsViaAccountDataUseCase,
                     canToggleNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase,
             )
 
@@ -70,7 +71,7 @@ class GetNotificationsStatusUseCaseTest {
     @Test
     fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
         // Given
-        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false)
         every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
 
         // When
@@ -80,7 +81,7 @@ class GetNotificationsStatusUseCaseTest {
         result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED
         verifyOrder {
             // we should first check account data
-            fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+            fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
             fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession)
         }
     }
@@ -95,7 +96,7 @@ class GetNotificationsStatusUseCaseTest {
                 )
         )
         fakeSession.pushersService().givenPushersLive(pushers)
-        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false)
         every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
 
         // When
@@ -109,7 +110,7 @@ class GetNotificationsStatusUseCaseTest {
     fun `given toggle via pusher is supported and no registered pusher when execute then resulting flow contains NOT_SUPPORTED value`() = runTest {
         // Given
         fakeSession.pushersService().givenPushersLive(emptyList())
-        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false
+        every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false)
         every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true)
 
         // When
@@ -120,7 +121,7 @@ class GetNotificationsStatusUseCaseTest {
     }
 
     @Test
-    fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest {
+    fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on account data`() = runTest {
         // Given
         fakeSession
                 .accountDataService()
@@ -130,7 +131,7 @@ class GetNotificationsStatusUseCaseTest {
                                 isSilenced = false
                         ).toContent(),
                 )
-        every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true
+        every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(true)
         every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false)
 
         // When
@@ -138,5 +139,11 @@ class GetNotificationsStatusUseCaseTest {
 
         // Then
         result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED
+        verify {
+            fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID)
+        }
+        verify(inverse = true) {
+            fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession)
+        }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
index c44fc4a497..f1a0ae7452 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
@@ -16,6 +16,8 @@
 
 package im.vector.app.test.fakes
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import io.mockk.coEvery
 import io.mockk.coVerify
 import io.mockk.every
@@ -25,6 +27,8 @@ import io.mockk.runs
 import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
 import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
 
 class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed = true) {
 
@@ -32,6 +36,13 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed
         every { getUserAccountDataEvent(type) } returns content?.let { UserAccountDataEvent(type, it) }
     }
 
+    fun givenGetLiveUserAccountDataEventReturns(type: String, content: Content?): LiveData> {
+        return MutableLiveData(content?.let { UserAccountDataEvent(type, it) }.toOptional())
+                .also {
+                    every { getLiveUserAccountDataEvent(type) } returns it
+                }
+    }
+
     fun givenUpdateUserAccountDataEventSucceeds() {
         coEvery { updateUserAccountData(any(), any()) } just runs
     }

From c12af5a800f5bd436482ec118052ecabb76a3460 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 2 Dec 2022 14:14:51 +0100
Subject: [PATCH 491/679] Listening changes on notifications enabled preference
 to update the UI in settings

---
 ...SettingsNotificationPreferenceViewModel.kt | 27 ++++++++++++++
 ...ingsNotificationPreferenceViewModelTest.kt | 35 +++++++++++++++++++
 .../app/test/fakes/FakeVectorPreferences.kt   |  7 ----
 3 files changed, 62 insertions(+), 7 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
index 48e82b35e8..9530be599e 100644
--- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt
@@ -16,6 +16,8 @@
 
 package im.vector.app.features.settings.notifications
 
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
 import com.airbnb.mvrx.MavericksViewModelFactory
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -50,6 +52,31 @@ class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor(
 
     companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
 
+    @VisibleForTesting
+    val notificationsPreferenceListener: SharedPreferences.OnSharedPreferenceChangeListener =
+            SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+                if (key == VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) {
+                    if (vectorPreferences.areNotificationEnabledForDevice()) {
+                        _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled)
+                    } else {
+                        _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled)
+                    }
+                }
+            }
+
+    init {
+        observeNotificationsEnabledPreference()
+    }
+
+    private fun observeNotificationsEnabledPreference() {
+        vectorPreferences.subscribeToChanges(notificationsPreferenceListener)
+    }
+
+    override fun onCleared() {
+        vectorPreferences.unsubscribeToChanges(notificationsPreferenceListener)
+        super.onCleared()
+    }
+
     override fun handle(action: VectorSettingsNotificationPreferenceViewAction) {
         when (action) {
             VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice()
diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
index 270447c461..ae36ee7600 100644
--- a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt
@@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorDummyViewState
 import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
 import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
 import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
+import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY
 import im.vector.app.test.fakes.FakePushersManager
 import im.vector.app.test.fakes.FakeVectorPreferences
 import im.vector.app.test.test
@@ -60,6 +61,40 @@ class VectorSettingsNotificationPreferenceViewModelTest {
             toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase,
     )
 
+    @Test
+    fun `given view model init when notifications are enabled in preferences then view event is posted`() {
+        // Given
+        fakeVectorPreferences.givenAreNotificationsEnabledForDevice(true)
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled
+        val viewModel = createViewModel()
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.notificationsPreferenceListener.onSharedPreferenceChanged(mockk(), SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+    }
+
+    @Test
+    fun `given view model init when notifications are disabled in preferences then view event is posted`() {
+        // Given
+        fakeVectorPreferences.givenAreNotificationsEnabledForDevice(false)
+        val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled
+        val viewModel = createViewModel()
+
+        // When
+        val viewModelTest = viewModel.test()
+        viewModel.notificationsPreferenceListener.onSharedPreferenceChanged(mockk(), SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)
+
+        // Then
+        viewModelTest
+                .assertEvent { event -> event == expectedEvent }
+                .finish()
+    }
+
     @Test
     fun `given DisableNotificationsForDevice action when handling action then disable use case is called`() {
         // Given
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
index 3d7de662bd..58bc1a18b8 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt
@@ -16,7 +16,6 @@
 
 package im.vector.app.test.fakes
 
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import im.vector.app.features.settings.BackgroundSyncMode
 import im.vector.app.features.settings.VectorPreferences
 import io.mockk.every
@@ -78,10 +77,4 @@ class FakeVectorPreferences {
     fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) {
         every { instance.isBackgroundSyncEnabled() } returns isEnabled
     }
-
-    fun givenChangeOnPreference(key: String) {
-        every { instance.subscribeToChanges(any()) } answers {
-            firstArg().onSharedPreferenceChanged(mockk(), key)
-        }
-    }
 }

From b8023d66debbcff06df6f1c6bdd47a8fc41a3f0c Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Mon, 5 Dec 2022 09:57:02 +0100
Subject: [PATCH 492/679] Fix formatting

---
 vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt | 1 -
 1 file changed, 1 deletion(-)

diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt
index 970bb15a25..a5cba20561 100644
--- a/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt
+++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt
@@ -32,4 +32,3 @@ fun createPaparazziRule() = Paparazzi(
         theme = "Theme.Vector.Light",
         maxPercentDifference = 0.0,
 )
-

From b4792c8a59e66715ee68f8ac650e712562b2b4a3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Dec 2022 10:20:02 +0100
Subject: [PATCH 493/679] Bump leakcanary-android from 2.9.1 to 2.10 (#7570)

---
 vector-app/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index 68e20996ad..fa6aa5f0fd 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -404,6 +404,6 @@ dependencies {
     androidTestImplementation libs.androidx.fragmentTesting
     androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
     debugImplementation libs.androidx.fragmentTesting
-    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
 }
 

From bbc756136cfa4b5d8dbc3baea00f88932b818b1f Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Mon, 5 Dec 2022 10:26:07 +0100
Subject: [PATCH 494/679] Adding the rename and signout actions in the menu

---
 .../v2/VectorSettingsDevicesFragment.kt       | 54 ++++++++++++-------
 .../res/menu/menu_current_session_header.xml  | 10 ++++
 2 files changed, 44 insertions(+), 20 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index d748600416..64744f6eeb 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -101,6 +101,7 @@ class VectorSettingsDevicesFragment :
 
         initWaitingView()
         initCurrentSessionHeaderView()
+        initCurrentSessionListView()
         initOtherSessionsHeaderView()
         initOtherSessionsView()
         initSecurityRecommendationsView()
@@ -153,6 +154,12 @@ class VectorSettingsDevicesFragment :
         }
     }
 
+    private fun initCurrentSessionListView() {
+        views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
+            viewModel.handle(DevicesAction.VerifyCurrentSession)
+        }
+    }
+
     private fun initOtherSessionsHeaderView() {
         views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem ->
             when (menuItem.itemId) {
@@ -351,31 +358,38 @@ class VectorSettingsDevicesFragment :
 
     private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) {
         currentDeviceInfo?.let {
-            views.deviceListHeaderCurrentSession.isVisible = true
-            val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
-            val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
-            signoutOtherSessionsItem.setTextColor(colorDestructive)
-            signoutOtherSessionsItem.isVisible = hasOtherDevices
-            views.deviceListCurrentSession.isVisible = true
-            val viewState = SessionInfoViewState(
-                    isCurrentSession = true,
-                    deviceFullInfo = it
-            )
-            views.deviceListCurrentSession.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
-            views.deviceListCurrentSession.debouncedClicks {
-                currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
-            }
-            views.deviceListCurrentSession.viewDetailsButton.debouncedClicks {
-                currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
-            }
-            views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
-                viewModel.handle(DevicesAction.VerifyCurrentSession)
-            }
+            renderCurrentSessionHeaderView(hasOtherDevices)
+            renderCurrentSessionListView(it)
         } ?: run {
             hideCurrentSessionView()
         }
     }
 
+    private fun renderCurrentSessionHeaderView(hasOtherDevices: Boolean) {
+        views.deviceListHeaderCurrentSession.isVisible = true
+        val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
+        val signoutSessionItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignout)
+        signoutSessionItem.setTextColor(colorDestructive)
+        val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
+        signoutOtherSessionsItem.setTextColor(colorDestructive)
+        signoutOtherSessionsItem.isVisible = hasOtherDevices
+    }
+
+    private fun renderCurrentSessionListView(currentDeviceInfo: DeviceFullInfo) {
+        views.deviceListCurrentSession.isVisible = true
+        val viewState = SessionInfoViewState(
+                isCurrentSession = true,
+                deviceFullInfo = currentDeviceInfo
+        )
+        views.deviceListCurrentSession.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
+        views.deviceListCurrentSession.debouncedClicks {
+            currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
+        }
+        views.deviceListCurrentSession.viewDetailsButton.debouncedClicks {
+            currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
+        }
+    }
+
     private fun navigateToSessionOverview(deviceId: String) {
         viewNavigator.navigateToSessionOverview(
                 context = requireActivity(),
diff --git a/vector/src/main/res/menu/menu_current_session_header.xml b/vector/src/main/res/menu/menu_current_session_header.xml
index 3b00423488..993bee6178 100644
--- a/vector/src/main/res/menu/menu_current_session_header.xml
+++ b/vector/src/main/res/menu/menu_current_session_header.xml
@@ -4,6 +4,16 @@
     xmlns:tools="http://schemas.android.com/tools"
     tools:ignore="AlwaysShowAction">
 
+    
+
+    
+
     
Date: Mon, 5 Dec 2022 09:42:20 +0000
Subject: [PATCH 495/679] Bump wysiwyg from 0.7.0.1 to 0.8.0 (#7666)

Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.7.0.1 to 0.8.0.
- [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases)
- [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/matrix-org/matrix-wysiwyg/commits/0.8.0)

---
updated-dependencies:
- dependency-name: io.element.android:wysiwyg
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index 27bca6434f..fd630eba6d 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -98,7 +98,7 @@ ext.libs = [
         ],
         element     : [
                 'opusencoder'             : "io.element.android:opusencoder:1.1.0",
-                'wysiwyg'                 : "io.element.android:wysiwyg:0.7.0.1"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.8.0"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",

From 540758d66b0730d408a9737142ecaa01b7ccfd93 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Mon, 5 Dec 2022 10:43:56 +0100
Subject: [PATCH 496/679] Navigate to rename session screen from current
 session menu

---
 .../v2/VectorSettingsDevicesFragment.kt       | 14 ++++++++
 .../v2/VectorSettingsDevicesViewNavigator.kt  |  5 +++
 .../VectorSettingsDevicesViewNavigatorTest.kt | 35 +++++++++++++++----
 .../OtherSessionsViewNavigatorTest.kt         |  8 ++---
 .../SessionOverviewViewNavigatorTest.kt       |  8 ++---
 .../im/vector/app/test/fakes/FakeContext.kt   | 10 ++++--
 6 files changed, 61 insertions(+), 19 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index 64744f6eeb..c29655a0c7 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -145,6 +145,10 @@ class VectorSettingsDevicesFragment :
     private fun initCurrentSessionHeaderView() {
         views.deviceListHeaderCurrentSession.setOnMenuItemClickListener { menuItem ->
             when (menuItem.itemId) {
+                R.id.currentSessionHeaderRename -> {
+                    navigateToRenameCurrentSession()
+                    true
+                }
                 R.id.currentSessionHeaderSignoutOtherSessions -> {
                     confirmMultiSignoutOtherSessions()
                     true
@@ -154,6 +158,16 @@ class VectorSettingsDevicesFragment :
         }
     }
 
+    private fun navigateToRenameCurrentSession() = withState(viewModel) { state ->
+        val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId
+        if (currentDeviceId.isNotEmpty()) {
+            viewNavigator.navigateToRenameSession(
+                    context = requireActivity(),
+                    deviceId = currentDeviceId,
+            )
+        }
+    }
+
     private fun initCurrentSessionListView() {
         views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
             viewModel.handle(DevicesAction.VerifyCurrentSession)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt
index 47e697822b..d4b3345fea 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt
@@ -20,6 +20,7 @@ import android.content.Context
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
 import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
 import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
+import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
 import javax.inject.Inject
 
 class VectorSettingsDevicesViewNavigator @Inject constructor() {
@@ -38,4 +39,8 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
                 OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice)
         )
     }
+
+    fun navigateToRenameSession(context: Context, deviceId: String) {
+        context.startActivity(RenameSessionActivity.newIntent(context, deviceId))
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
index ec8019384a..37823f7d53 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
@@ -20,6 +20,7 @@ import android.content.Intent
 import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
 import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
 import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
+import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
 import im.vector.app.test.fakes.FakeContext
 import io.mockk.every
 import io.mockk.mockk
@@ -43,6 +44,7 @@ class VectorSettingsDevicesViewNavigatorTest {
     fun setUp() {
         mockkObject(SessionOverviewActivity.Companion)
         mockkObject(OtherSessionsActivity.Companion)
+        mockkObject(RenameSessionActivity.Companion)
     }
 
     @After
@@ -52,26 +54,41 @@ class VectorSettingsDevicesViewNavigatorTest {
 
     @Test
     fun `given a session id when navigating to overview then it starts the correct activity`() {
+        // Given
         val intent = givenIntentForSessionOverview(A_SESSION_ID)
         context.givenStartActivity(intent)
 
+        // When
         vectorSettingsDevicesViewNavigator.navigateToSessionOverview(context.instance, A_SESSION_ID)
 
-        verify {
-            context.instance.startActivity(intent)
-        }
+        // Then
+        context.verifyStartActivity(intent)
     }
 
     @Test
     fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
+        // Given
         val intent = givenIntentForOtherSessions(A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
         context.givenStartActivity(intent)
 
+        // When
         vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
 
-        verify {
-            context.instance.startActivity(intent)
-        }
+        // Then
+        context.verifyStartActivity(intent)
+    }
+
+    @Test
+    fun `given an intent when navigating to rename session screen then it starts the correct activity`() {
+        // Given
+        val intent = givenIntentForRenameSession(A_SESSION_ID)
+        context.givenStartActivity(intent)
+
+        // When
+        vectorSettingsDevicesViewNavigator.navigateToRenameSession(context.instance, A_SESSION_ID)
+
+        // Then
+        context.verifyStartActivity(intent)
     }
 
     private fun givenIntentForSessionOverview(sessionId: String): Intent {
@@ -85,4 +102,10 @@ class VectorSettingsDevicesViewNavigatorTest {
         every { OtherSessionsActivity.newIntent(context.instance, titleResourceId, defaultFilter, excludeCurrentDevice) } returns intent
         return intent
     }
+
+    private fun givenIntentForRenameSession(sessionId: String): Intent {
+        val intent = mockk()
+        every { RenameSessionActivity.newIntent(context.instance, sessionId) } returns intent
+        return intent
+    }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt
index 3123572521..23fc1cbdce 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigatorTest.kt
@@ -23,7 +23,6 @@ import io.mockk.every
 import io.mockk.mockk
 import io.mockk.mockkObject
 import io.mockk.unmockkAll
-import io.mockk.verify
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -47,14 +46,15 @@ class OtherSessionsViewNavigatorTest {
 
     @Test
     fun `given a device id when navigating to overview then it starts the correct activity`() {
+        // Given
         val intent = givenIntentForDeviceOverview(A_DEVICE_ID)
         context.givenStartActivity(intent)
 
+        // When
         otherSessionsViewNavigator.navigateToSessionOverview(context.instance, A_DEVICE_ID)
 
-        verify {
-            context.instance.startActivity(intent)
-        }
+        // Then
+        context.verifyStartActivity(intent)
     }
 
     private fun givenIntentForDeviceOverview(deviceId: String): Intent {
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt
index e309c05042..99f3013697 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewNavigatorTest.kt
@@ -60,9 +60,7 @@ class SessionOverviewViewNavigatorTest {
         sessionOverviewViewNavigator.goToSessionDetails(context.instance, A_SESSION_ID)
 
         // Then
-        verify {
-            context.instance.startActivity(intent)
-        }
+        context.verifyStartActivity(intent)
     }
 
     @Test
@@ -75,9 +73,7 @@ class SessionOverviewViewNavigatorTest {
         sessionOverviewViewNavigator.goToRenameSession(context.instance, A_SESSION_ID)
 
         // Then
-        verify {
-            context.instance.startActivity(intent)
-        }
+        context.verifyStartActivity(intent)
     }
 
     @Test
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
index 9a94313fec..22e191e29c 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
@@ -24,9 +24,9 @@ import android.net.ConnectivityManager
 import android.net.Uri
 import android.os.ParcelFileDescriptor
 import io.mockk.every
-import io.mockk.just
+import io.mockk.justRun
 import io.mockk.mockk
-import io.mockk.runs
+import io.mockk.verify
 import java.io.OutputStream
 
 class FakeContext(
@@ -73,7 +73,11 @@ class FakeContext(
     }
 
     fun givenStartActivity(intent: Intent) {
-        every { instance.startActivity(intent) } just runs
+        justRun { instance.startActivity(intent) }
+    }
+
+    fun verifyStartActivity(intent: Intent) {
+        verify { instance.startActivity(intent) }
     }
 
     fun givenClipboardManager(): FakeClipboardManager {

From 57554c5d3611642023669279d65d4c8c909b2b51 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Mon, 5 Dec 2022 14:10:56 +0100
Subject: [PATCH 497/679] Handling signout current session action

---
 .../settings/devices/v2/VectorSettingsDevicesFragment.kt | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index c29655a0c7..a21c7accb7 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -52,6 +52,7 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie
 import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
 import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
 import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
+import im.vector.app.features.workers.signout.SignOutUiWorker
 import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@@ -149,6 +150,10 @@ class VectorSettingsDevicesFragment :
                     navigateToRenameCurrentSession()
                     true
                 }
+                R.id.currentSessionHeaderSignout -> {
+                    confirmSignoutCurrentSession()
+                    true
+                }
                 R.id.currentSessionHeaderSignoutOtherSessions -> {
                     confirmMultiSignoutOtherSessions()
                     true
@@ -168,6 +173,10 @@ class VectorSettingsDevicesFragment :
         }
     }
 
+    private fun confirmSignoutCurrentSession() {
+        activity?.let { SignOutUiWorker(it).perform() }
+    }
+
     private fun initCurrentSessionListView() {
         views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
             viewModel.handle(DevicesAction.VerifyCurrentSession)

From a00508e08588568c8a85d14aa0f591ca31393b60 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Mon, 5 Dec 2022 14:12:00 +0100
Subject: [PATCH 498/679] Removing unused import

---
 .../devices/v2/VectorSettingsDevicesViewNavigatorTest.kt         | 1 -
 1 file changed, 1 deletion(-)

diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
index 37823f7d53..24582c75d8 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
@@ -26,7 +26,6 @@ import io.mockk.every
 import io.mockk.mockk
 import io.mockk.mockkObject
 import io.mockk.unmockkAll
-import io.mockk.verify
 import org.junit.After
 import org.junit.Before
 import org.junit.Test

From 516103e51b2aa132f577166e2a004acaed57bbb1 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Mon, 5 Dec 2022 18:10:22 +0300
Subject: [PATCH 499/679] Fix usage of unknown shield in room summary.

---
 .../app/core/ui/views/ShieldImageView.kt      | 25 ++++++++++++++++---
 .../features/settings/devices/DeviceItem.kt   |  4 +--
 .../devices/v2/list/OtherSessionItem.kt       |  2 +-
 3 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
index 6327daec86..0570bbe4d7 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
@@ -38,6 +38,25 @@ class ShieldImageView @JvmOverloads constructor(
         }
     }
 
+    /**
+     * Renders device shield with the support of unknown shields instead of black shields which is used for rooms.
+     * @param roomEncryptionTrustLevel trust level that is usally calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust]
+     * @param borderLess if true then the shield icon with border around is used
+     */
+    fun renderDeviceShield(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
+        isVisible = roomEncryptionTrustLevel != null
+
+        if (roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Default) {
+            contentDescription = context.getString(R.string.a11y_trust_level_default)
+            setImageResource(
+                    if (borderLess) R.drawable.ic_shield_unknown_no_border
+                    else R.drawable.ic_shield_unknown
+            )
+        } else {
+            render(roomEncryptionTrustLevel, borderLess)
+        }
+    }
+
     fun render(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
         isVisible = roomEncryptionTrustLevel != null
 
@@ -45,8 +64,8 @@ class ShieldImageView @JvmOverloads constructor(
             RoomEncryptionTrustLevel.Default -> {
                 contentDescription = context.getString(R.string.a11y_trust_level_default)
                 setImageResource(
-                        if (borderLess) R.drawable.ic_shield_unknown_no_border
-                        else R.drawable.ic_shield_unknown
+                        if (borderLess) R.drawable.ic_shield_black_no_border
+                        else R.drawable.ic_shield_black
                 )
             }
             RoomEncryptionTrustLevel.Warning -> {
@@ -137,7 +156,7 @@ class ShieldImageView @JvmOverloads constructor(
 @DrawableRes
 fun RoomEncryptionTrustLevel.toDrawableRes(): Int {
     return when (this) {
-        RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_unknown
+        RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black
         RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
         RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted
         RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt
index 6486b8a3ca..5924742c26 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt
@@ -85,9 +85,9 @@ abstract class DeviceItem : VectorEpoxyModel(R.layout.item_de
                     trusted
             )
 
-            holder.trustIcon.render(shield)
+            holder.trustIcon.renderDeviceShield(shield)
         } else {
-            holder.trustIcon.render(null)
+            holder.trustIcon.renderDeviceShield(null)
         }
 
         val detailedModeLabels = listOf(
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
index 9d9cb15c28..68cae344cd 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
@@ -97,7 +97,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la
         } else {
             setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider)
         }
-        holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel)
+        holder.otherSessionVerificationStatusImageView.renderDeviceShield(roomEncryptionTrustLevel)
         holder.otherSessionNameTextView.text = sessionName
         holder.otherSessionDescriptionTextView.text = sessionDescription
         sessionDescriptionColor?.let {

From 32ded289fcf91b46c45df3c6cbfa92dbddb71d29 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Mon, 5 Dec 2022 18:18:09 +0300
Subject: [PATCH 500/679] Add changelog.

---
 changelog.d/7710.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7710.bugfix

diff --git a/changelog.d/7710.bugfix b/changelog.d/7710.bugfix
new file mode 100644
index 0000000000..9e75a03e1b
--- /dev/null
+++ b/changelog.d/7710.bugfix
@@ -0,0 +1 @@
+Fix usage of unknown shield in room summary

From febf01a2e634c51fc4cdf1c1ff64d26230731369 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Mon, 5 Dec 2022 16:36:16 +0100
Subject: [PATCH 501/679] Use the API `startForeground(int id, @NonNull
 Notification notification, @ForegroundServiceType int foregroundServiceType)`
 when available.

Add missing android:foregroundServiceType in the manifest
---
 vector/src/main/AndroidManifest.xml           |  6 ++-
 .../im/vector/app/core/extensions/Service.kt  | 38 +++++++++++++++++++
 .../app/core/services/CallAndroidService.kt   | 13 ++++---
 .../core/services/VectorSyncAndroidService.kt |  3 +-
 .../webrtc/ScreenCaptureAndroidService.kt     |  3 +-
 .../tracking/LocationSharingAndroidService.kt |  3 +-
 .../features/start/StartAppAndroidService.kt  |  3 +-
 7 files changed, 58 insertions(+), 11 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/core/extensions/Service.kt

diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index a26be23456..9c8186b2d4 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -72,7 +72,9 @@
     
 
         
-        
+        
 
         
         
             
             
@@ -341,6 +344,7 @@
         
 
          Int)? = null
+) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+        startForeground(
+                id,
+                notification,
+                provideForegroundServiceType?.invoke() ?: ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
+        )
+    } else {
+        startForeground(id, notification)
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt
index 85ea7f1a1b..a4e3872e0f 100644
--- a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt
+++ b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt
@@ -28,6 +28,7 @@ import androidx.media.session.MediaButtonReceiver
 import com.airbnb.mvrx.Mavericks
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.core.extensions.singletonEntryPoint
+import im.vector.app.core.extensions.startForegroundCompat
 import im.vector.app.features.call.CallArgs
 import im.vector.app.features.call.VectorCallActivity
 import im.vector.app.features.call.telecom.CallConnection
@@ -181,7 +182,7 @@ class CallAndroidService : VectorAndroidService() {
                 fromBg = fromBg
         )
         if (knownCalls.isEmpty()) {
-            startForeground(callId.hashCode(), notification)
+            startForegroundCompat(callId.hashCode(), notification)
         } else {
             notificationManager.notify(callId.hashCode(), notification)
         }
@@ -201,7 +202,7 @@ class CallAndroidService : VectorAndroidService() {
         }
         val notification = notificationUtils.buildCallEndedNotification(false)
         val notificationId = callId.hashCode()
-        startForeground(notificationId, notification)
+        startForegroundCompat(notificationId, notification)
         if (knownCalls.isEmpty()) {
             Timber.tag(loggerTag.value).v("No more call, stop the service")
             stopForegroundCompat()
@@ -236,7 +237,7 @@ class CallAndroidService : VectorAndroidService() {
                 title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
         )
         if (knownCalls.isEmpty()) {
-            startForeground(callId.hashCode(), notification)
+            startForegroundCompat(callId.hashCode(), notification)
         } else {
             notificationManager.notify(callId.hashCode(), notification)
         }
@@ -260,7 +261,7 @@ class CallAndroidService : VectorAndroidService() {
                 title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
         )
         if (knownCalls.isEmpty()) {
-            startForeground(callId.hashCode(), notification)
+            startForegroundCompat(callId.hashCode(), notification)
         } else {
             notificationManager.notify(callId.hashCode(), notification)
         }
@@ -273,9 +274,9 @@ class CallAndroidService : VectorAndroidService() {
         callRingPlayerOutgoing?.stop()
         val notification = notificationUtils.buildCallEndedNotification(false)
         if (callId != null) {
-            startForeground(callId.hashCode(), notification)
+            startForegroundCompat(callId.hashCode(), notification)
         } else {
-            startForeground(DEFAULT_NOTIFICATION_ID, notification)
+            startForegroundCompat(DEFAULT_NOTIFICATION_ID, notification)
         }
         if (knownCalls.isEmpty()) {
             mediaSession?.isActive = false
diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt
index 864f69a136..f746c0749b 100644
--- a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt
+++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt
@@ -32,6 +32,7 @@ import androidx.work.Worker
 import androidx.work.WorkerParameters
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.R
+import im.vector.app.core.extensions.startForegroundCompat
 import im.vector.app.core.platform.PendingIntentCompat
 import im.vector.app.core.time.Clock
 import im.vector.app.core.time.DefaultClock
@@ -98,7 +99,7 @@ class VectorSyncAndroidService : SyncAndroidService() {
             R.string.notification_listening_for_notifications
         }
         val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
-        startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
+        startForegroundCompat(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
     }
 
     override fun onRescheduleAsked(
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt
index e7cebfb9c9..00b6bc40d2 100644
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt
+++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt
@@ -20,6 +20,7 @@ import android.content.Intent
 import android.os.Binder
 import android.os.IBinder
 import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.extensions.startForegroundCompat
 import im.vector.app.core.services.VectorAndroidService
 import im.vector.app.core.time.Clock
 import im.vector.app.features.notifications.NotificationUtils
@@ -41,7 +42,7 @@ class ScreenCaptureAndroidService : VectorAndroidService() {
     private fun showStickyNotification() {
         val notificationId = clock.epochMillis().toInt()
         val notification = notificationUtils.buildScreenSharingNotification()
-        startForeground(notificationId, notification)
+        startForegroundCompat(notificationId, notification)
     }
 
     override fun onBind(intent: Intent?): IBinder {
diff --git a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt
index ccab23a83b..d77a87f756 100644
--- a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt
+++ b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt
@@ -22,6 +22,7 @@ import android.os.Parcelable
 import androidx.core.app.NotificationManagerCompat
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.extensions.startForegroundCompat
 import im.vector.app.core.services.VectorAndroidService
 import im.vector.app.features.location.LocationData
 import im.vector.app.features.location.LocationTracker
@@ -105,7 +106,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
             if (foregroundModeStarted) {
                 NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
             } else {
-                startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
+                startForegroundCompat(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
                 foregroundModeStarted = true
             }
 
diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt
index e8e0eac863..a14967a931 100644
--- a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt
+++ b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt
@@ -20,6 +20,7 @@ import android.content.Intent
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.di.NamedGlobalScope
+import im.vector.app.core.extensions.startForegroundCompat
 import im.vector.app.core.services.VectorAndroidService
 import im.vector.app.features.notifications.NotificationUtils
 import kotlinx.coroutines.CoroutineScope
@@ -58,6 +59,6 @@ class StartAppAndroidService : VectorAndroidService() {
     private fun showStickyNotification() {
         val notificationId = Random.nextInt()
         val notification = notificationUtils.buildStartAppNotification()
-        startForeground(notificationId, notification)
+        startForegroundCompat(notificationId, notification)
     }
 }

From 7b830d1c1a0679977ba5b2f2155f71aa86a4b04e Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Mon, 5 Dec 2022 17:40:38 +0100
Subject: [PATCH 502/679] Renaming a use case

---
 ...anToggleNotificationsViaAccountDataUseCase.kt |  4 ++--
 ...ficationSettingsAccountDataUpdatesUseCase.kt} |  2 +-
 ...ggleNotificationsViaAccountDataUseCaseTest.kt | 16 ++++++++--------
 ...tionSettingsAccountDataUpdatesUseCaseTest.kt} | 10 +++++-----
 4 files changed, 16 insertions(+), 16 deletions(-)
 rename vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/{GetNotificationSettingsAccountDataAsFlowUseCase.kt => GetNotificationSettingsAccountDataUpdatesUseCase.kt} (94%)
 rename vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/{GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt => GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt} (87%)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt
index ac466852eb..18ee9ad937 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt
@@ -22,11 +22,11 @@ import org.matrix.android.sdk.api.session.Session
 import javax.inject.Inject
 
 class CanToggleNotificationsViaAccountDataUseCase @Inject constructor(
-        private val getNotificationSettingsAccountDataAsFlowUseCase: GetNotificationSettingsAccountDataAsFlowUseCase,
+        private val getNotificationSettingsAccountDataUpdatesUseCase: GetNotificationSettingsAccountDataUpdatesUseCase,
 ) {
 
     fun execute(session: Session, deviceId: String): Flow {
-        return getNotificationSettingsAccountDataAsFlowUseCase.execute(session, deviceId)
+        return getNotificationSettingsAccountDataUpdatesUseCase.execute(session, deviceId)
                 .map { it?.isSilenced != null }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt
similarity index 94%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt
index ea4bd40f1f..308aeec5f2 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt
@@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.events.model.toModel
 import javax.inject.Inject
 
-class GetNotificationSettingsAccountDataAsFlowUseCase @Inject constructor() {
+class GetNotificationSettingsAccountDataUpdatesUseCase @Inject constructor() {
 
     fun execute(session: Session, deviceId: String): Flow {
         return session
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt
index a1dfed6902..b85acb1e69 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt
@@ -29,10 +29,10 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 
 class CanToggleNotificationsViaAccountDataUseCaseTest {
 
-    private val fakeGetNotificationSettingsAccountDataAsFlowUseCase = mockk()
+    private val fakeGetNotificationSettingsAccountDataUpdatesUseCase = mockk()
 
     private val canToggleNotificationsViaAccountDataUseCase = CanToggleNotificationsViaAccountDataUseCase(
-            getNotificationSettingsAccountDataAsFlowUseCase = fakeGetNotificationSettingsAccountDataAsFlowUseCase,
+            getNotificationSettingsAccountDataUpdatesUseCase = fakeGetNotificationSettingsAccountDataUpdatesUseCase,
     )
 
     @Test
@@ -43,14 +43,14 @@ class CanToggleNotificationsViaAccountDataUseCaseTest {
         val localNotificationSettingsContent = LocalNotificationSettingsContent(
                 isSilenced = true,
         )
-        every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent)
+        every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent)
 
         // When
         val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull()
 
         // Then
         result shouldBe true
-        verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) }
+        verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) }
     }
 
     @Test
@@ -61,14 +61,14 @@ class CanToggleNotificationsViaAccountDataUseCaseTest {
         val localNotificationSettingsContent = LocalNotificationSettingsContent(
                 isSilenced = null,
         )
-        every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent)
+        every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent)
 
         // When
         val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull()
 
         // Then
         result shouldBe false
-        verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) }
+        verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) }
     }
 
     @Test
@@ -76,13 +76,13 @@ class CanToggleNotificationsViaAccountDataUseCaseTest {
         // Given
         val aSession = FakeSession()
         val aDeviceId = "aDeviceId"
-        every { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(any(), any()) } returns flowOf(null)
+        every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(null)
 
         // When
         val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull()
 
         // Then
         result shouldBe false
-        verify { fakeGetNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId) }
+        verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) }
     }
 }
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt
similarity index 87%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt
index 6280d4c48b..50940b9d34 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataAsFlowUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt
@@ -30,10 +30,10 @@ import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.events.model.toContent
 
-class GetNotificationSettingsAccountDataAsFlowUseCaseTest {
+class GetNotificationSettingsAccountDataUpdatesUseCaseTest {
 
     private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
-    private val getNotificationSettingsAccountDataAsFlowUseCase = GetNotificationSettingsAccountDataAsFlowUseCase()
+    private val getNotificationSettingsAccountDataUpdatesUseCase = GetNotificationSettingsAccountDataUpdatesUseCase()
 
     @Before
     fun setUp() {
@@ -60,7 +60,7 @@ class GetNotificationSettingsAccountDataAsFlowUseCaseTest {
                 .givenAsFlow()
 
         // When
-        val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull()
+        val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull()
 
         // Then
         result shouldBeEqualTo expectedContent
@@ -80,7 +80,7 @@ class GetNotificationSettingsAccountDataAsFlowUseCaseTest {
                 .givenAsFlow()
 
         // When
-        val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull()
+        val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull()
 
         // Then
         result shouldBeEqualTo null
@@ -101,7 +101,7 @@ class GetNotificationSettingsAccountDataAsFlowUseCaseTest {
                 .givenAsFlow()
 
         // When
-        val result = getNotificationSettingsAccountDataAsFlowUseCase.execute(aSession, aDeviceId).firstOrNull()
+        val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull()
 
         // Then
         result shouldBeEqualTo expectedContent

From f2952f2deee61693de49e6b703b913ba0d5d8d6d Mon Sep 17 00:00:00 2001
From: valere 
Date: Mon, 5 Dec 2022 18:15:30 +0100
Subject: [PATCH 503/679] add to device tracing id

---
 changelog.d/7708.misc                         |   1 +
 .../internal/crypto/tasks/SendToDeviceTask.kt |  44 ++++-
 .../session/sync/handler/CryptoSyncHandler.kt |   5 +-
 .../crypto/DefaultSendToDeviceTaskTest.kt     | 161 ++++++++++++++++++
 4 files changed, 206 insertions(+), 5 deletions(-)
 create mode 100644 changelog.d/7708.misc
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt

diff --git a/changelog.d/7708.misc b/changelog.d/7708.misc
new file mode 100644
index 0000000000..6273330395
--- /dev/null
+++ b/changelog.d/7708.misc
@@ -0,0 +1 @@
+Add tracing Id for to device messages
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
index fc4d422360..1e6ceeb138 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
@@ -17,14 +17,21 @@
 package org.matrix.android.sdk.internal.crypto.tasks
 
 import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
 import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
+import timber.log.Timber
 import java.util.UUID
 import javax.inject.Inject
 
+const val TO_DEVICE_TRACING_ID_KEY = "org.matrix.msgid"
+
+fun Event.toDeviceTracingId(): String? = content?.get(TO_DEVICE_TRACING_ID_KEY) as? String
+
 internal interface SendToDeviceTask : Task {
     data class Params(
             // the type of event to send
@@ -42,15 +49,17 @@ internal class DefaultSendToDeviceTask @Inject constructor(
 ) : SendToDeviceTask {
 
     override suspend fun execute(params: SendToDeviceTask.Params) {
-        val sendToDeviceBody = SendToDeviceBody(
-                messages = params.contentMap.map
-        )
-
         // If params.transactionId is not provided, we create a unique txnId.
         // It's important to do that outside the requestBlock parameter of executeRequest()
         // to use the same value if the request is retried
         val txnId = params.transactionId ?: createUniqueTxnId()
 
+        // add id tracing to debug
+        val decorated = decorateWithToDeviceTracingIds(params)
+        val sendToDeviceBody = SendToDeviceBody(
+                messages = decorated.first
+        )
+
         return executeRequest(
                 globalErrorReceiver,
                 canRetry = true,
@@ -61,8 +70,35 @@ internal class DefaultSendToDeviceTask @Inject constructor(
                     transactionId = txnId,
                     body = sendToDeviceBody
             )
+            Timber.i("Sent to device type=${params.eventType} txnid=$txnId [${decorated.second.joinToString(",")}]")
         }
     }
+
+    /**
+     * To make it easier to track down where to-device messages are getting lost,
+     * add a custom property to each one, and that will be logged after sent and on reception. Synapse will also log
+     * this property.
+     * @return A pair, first is the decorated content, and second info to log out after sending
+     */
+    private fun decorateWithToDeviceTracingIds(params: SendToDeviceTask.Params): Pair>, List> {
+        val tracingInfo = mutableListOf()
+        val decoratedContent = params.contentMap.map.map { userToDeviceMap ->
+            val userId = userToDeviceMap.key
+            userId to userToDeviceMap.value.map {
+                val deviceId = it.key
+                deviceId to it.value.toContent().toMutableMap().apply {
+                    put(
+                            TO_DEVICE_TRACING_ID_KEY,
+                            UUID.randomUUID().toString().also {
+                                tracingInfo.add("$userId/$deviceId (msgid $it)")
+                            }
+                    )
+                }
+            }.toMap()
+        }.toMap()
+
+        return decoratedContent to tracingInfo
+    }
 }
 
 internal fun createUniqueTxnId() = UUID.randomUUID().toString()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
index b2fe12ebc3..291e785aa5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId
 import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
 import org.matrix.android.sdk.internal.session.sync.ProgressReporter
 import timber.log.Timber
@@ -48,12 +49,14 @@ internal class CryptoSyncHandler @Inject constructor(
                 ?.forEachIndexed { index, event ->
                     progressReporter?.reportProgress(index * 100F / total)
                     // Decrypt event if necessary
-                    Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
+                    Timber.tag(loggerTag.value).d("To device event tracingId:${event.toDeviceTracingId()}")
                     decryptToDeviceEvent(event, null)
+
                     if (event.getClearType() == EventType.MESSAGE &&
                             event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") {
                         Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
                     } else {
+                        Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} id:${event.toDeviceTracingId()}")
                         verificationService.onToDeviceEvent(event)
                         cryptoService.onToDeviceEvent(event)
                     }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
new file mode 100644
index 0000000000..72d166c1f4
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
+import org.amshove.kluent.internal.assertEquals
+import org.junit.Assert
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.internal.crypto.api.CryptoApi
+import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
+import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
+import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse
+import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
+import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
+import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody
+import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
+import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody
+import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
+import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
+import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse
+import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody
+import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
+import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
+import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+
+class DefaultSendToDeviceTaskTest {
+
+    val users = listOf(
+            "@alice:example.com" to listOf("D0", "D1"),
+            "bob@example.com" to listOf("D2", "D3")
+    )
+
+    val fakeEncryptedContent = mapOf(
+            "algorithm" to "m.olm.v1.curve25519-aes-sha2",
+            "sender_key" to "gMObR+/4dqL5T4DisRRRYBJpn+OjzFnkyCFOktP6Eyw",
+            "ciphertext" to mapOf(
+                    "tdwXf7006FDgzmufMCVI4rDdVPO51ecRTTT6HkRxUwE" to mapOf(
+                            "type" to 0,
+                            "body" to "AwogCA1ULEc0abGIFxMDIC9iv7ul3jqJSnapTHQ+8JJx"
+                    )
+            )
+    )
+
+    @Test
+    fun `tracing id should be added to all to_device contents`() {
+        val fakeCryptoAPi = FakeCryptoApi()
+
+        val sendToDeviceTask = DefaultSendToDeviceTask(
+                cryptoApi = fakeCryptoAPi,
+                globalErrorReceiver = mockk(relaxed = true)
+        )
+
+        val contentMap = MXUsersDevicesMap()
+
+        users.forEach {
+            val userId = it.first
+            it.second.forEach {
+                contentMap.setObject(userId, it, fakeEncryptedContent)
+            }
+        }
+
+        val params = SendToDeviceTask.Params(
+                eventType = EventType.ENCRYPTED,
+                contentMap = contentMap
+        )
+
+        runBlocking {
+            sendToDeviceTask.execute(params)
+        }
+
+        val generatedIds = mutableListOf()
+        users.forEach {
+            val userId = it.first
+            it.second.forEach {
+                val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map
+                Assert.assertNotNull("Tracing id should have been added", modifiedContent["org.matrix.msgid"])
+                generatedIds.add(modifiedContent["org.matrix.msgid"] as String)
+
+                assertEquals(
+                        "The rest of the content should be the same",
+                        fakeEncryptedContent.keys,
+                        modifiedContent.toMutableMap().apply { remove("org.matrix.msgid") }.keys
+                )
+            }
+        }
+
+        assertEquals("Id should be unique per content", generatedIds.size, generatedIds.toSet().size)
+        println("modified content ${fakeCryptoAPi.body}")
+    }
+
+    internal class FakeCryptoApi : CryptoApi {
+        override suspend fun getDevices(): DevicesListResponse {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun getDeviceInfo(deviceId: String): DeviceInfo {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun uploadKeys(body: KeysUploadBody): KeysUploadResponse {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun downloadKeysForUsers(params: KeysQueryBody): KeysQueryResponse {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun uploadSigningKeys(params: UploadSigningKeysBody): KeysQueryResponse {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun uploadSignatures(params: Map?): SignatureUploadResponse {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun claimOneTimeKeysForUsersDevices(body: KeysClaimBody): KeysClaimResponse {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        var body: SendToDeviceBody? = null
+        override suspend fun sendToDevice(eventType: String, transactionId: String, body: SendToDeviceBody) {
+            this.body = body
+        }
+
+        override suspend fun deleteDevice(deviceId: String, params: DeleteDeviceParams) {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun deleteDevices(params: DeleteDevicesParams) {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun updateDeviceInfo(deviceId: String, params: UpdateDeviceInfoBody) {
+            throw java.lang.AssertionError("Should not be called")
+        }
+
+        override suspend fun getKeyChanges(oldToken: String, newToken: String): KeyChangesResponse {
+            throw java.lang.AssertionError("Should not be called")
+        }
+    }
+}

From 2ed212aa11a36aed1df67722df77f8736f96ad4f Mon Sep 17 00:00:00 2001
From: valere 
Date: Mon, 5 Dec 2022 18:30:38 +0100
Subject: [PATCH 504/679] Fix copyright

---
 .../android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
index 72d166c1f4..c813991d3f 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.

From 139eb1708c3b5e7f9fb15d87c6fb2dd3346f8639 Mon Sep 17 00:00:00 2001
From: valere 
Date: Tue, 6 Dec 2022 08:17:31 +0100
Subject: [PATCH 505/679] fix uncheck cast warning

---
 .../crypto/DefaultSendToDeviceTaskTest.kt     | 21 +++++++++----------
 1 file changed, 10 insertions(+), 11 deletions(-)

diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
index c813991d3f..b8e870bd06 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
@@ -41,16 +41,15 @@ import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody
 import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
 import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
-import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 
 class DefaultSendToDeviceTaskTest {
 
-    val users = listOf(
+    private val users = listOf(
             "@alice:example.com" to listOf("D0", "D1"),
             "bob@example.com" to listOf("D2", "D3")
     )
 
-    val fakeEncryptedContent = mapOf(
+    private val fakeEncryptedContent = mapOf(
             "algorithm" to "m.olm.v1.curve25519-aes-sha2",
             "sender_key" to "gMObR+/4dqL5T4DisRRRYBJpn+OjzFnkyCFOktP6Eyw",
             "ciphertext" to mapOf(
@@ -67,14 +66,14 @@ class DefaultSendToDeviceTaskTest {
 
         val sendToDeviceTask = DefaultSendToDeviceTask(
                 cryptoApi = fakeCryptoAPi,
-                globalErrorReceiver = mockk(relaxed = true)
+                globalErrorReceiver = mockk(relaxed = true)
         )
 
         val contentMap = MXUsersDevicesMap()
 
-        users.forEach {
-            val userId = it.first
-            it.second.forEach {
+        users.forEach { pairOfUserDevices ->
+            val userId = pairOfUserDevices.first
+            pairOfUserDevices.second.forEach {
                 contentMap.setObject(userId, it, fakeEncryptedContent)
             }
         }
@@ -89,10 +88,10 @@ class DefaultSendToDeviceTaskTest {
         }
 
         val generatedIds = mutableListOf()
-        users.forEach {
-            val userId = it.first
-            it.second.forEach {
-                val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map
+        users.forEach { pairOfUserDevices ->
+            val userId = pairOfUserDevices.first
+            pairOfUserDevices.second.forEach {
+                val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map<*, *>
                 Assert.assertNotNull("Tracing id should have been added", modifiedContent["org.matrix.msgid"])
                 generatedIds.add(modifiedContent["org.matrix.msgid"] as String)
 

From 4cd4cf1c51930954e22398f21dcf0312e14e0c05 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Tue, 6 Dec 2022 14:06:14 +0300
Subject: [PATCH 506/679] Code review fix.

---
 .../app/features/settings/devices/v2/list/SessionInfoView.kt    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
index c6044d04a4..7727cee4fa 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
@@ -90,7 +90,7 @@ class SessionInfoView @JvmOverloads constructor(
             hasLearnMoreLink: Boolean,
             isVerifyButtonVisible: Boolean,
     ) {
-        views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel)
+        views.sessionInfoVerificationStatusImageView.renderDeviceShield(encryptionTrustLevel)
         when {
             encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession)
             encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown()

From a65e13970d9ca9a92feb913ec0e5ab71ddaac3d2 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Tue, 6 Dec 2022 11:52:13 +0100
Subject: [PATCH 507/679] `appdistribution` is only for nightly builds, not
 necessary for gplay (prod) builds.

---
 vector-app/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index fa6aa5f0fd..6ebb08600b 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -374,7 +374,7 @@ dependencies {
     // API-only library
     gplayImplementation libs.google.appdistributionApi
     // Full SDK implementation
-    gplayImplementation libs.google.appdistribution
+    nightlyImplementation libs.google.appdistribution
 
     // OSS License, gplay flavor only
     gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'

From 0d12dbbe7e8031aba0ca64e4da514c9e79636e01 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Tue, 6 Dec 2022 12:21:07 +0100
Subject: [PATCH 508/679] Disable the Nightly popup, user registration (with
 `updateIfNewReleaseAvailable()`) to get upgrade does not work.

Add a nightly build section in the preferences to manually try to upgrade.
---
 .../src/main/res/values/strings.xml           |  3 +++
 .../app/nightly/FirebaseNightlyProxy.kt       | 21 +++++++++++++++----
 .../vector/app/features/home/HomeActivity.kt  |  4 +++-
 .../vector/app/features/home/NightlyProxy.kt  | 15 ++++++++++++-
 .../VectorSettingsAdvancedSettingsFragment.kt | 18 ++++++++++++++++
 .../xml/vector_settings_advanced_settings.xml | 12 +++++++++++
 6 files changed, 67 insertions(+), 6 deletions(-)

diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 609cdac233..7f11e63469 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -2487,6 +2487,9 @@
     Key Requests
     Export Audit
 
+    Nightly build
+    Get the latest build (note: you may have trouble to sign in)
+
     Unlock encrypted messages history
 
     Refresh
diff --git a/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt b/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt
index 94a36036b6..71ffda7c36 100644
--- a/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt
+++ b/vector-app/src/gplay/java/im/vector/app/nightly/FirebaseNightlyProxy.kt
@@ -34,8 +34,11 @@ class FirebaseNightlyProxy @Inject constructor(
         private val buildMeta: BuildMeta,
 ) : NightlyProxy {
 
-    override fun onHomeResumed() {
-        if (!canDisplayPopup()) return
+    override fun isNightlyBuild(): Boolean {
+        return buildMeta.applicationId in nightlyPackages
+    }
+
+    override fun updateApplication() {
         val firebaseAppDistribution = FirebaseAppDistribution.getInstance()
         firebaseAppDistribution.updateIfNewReleaseAvailable()
                 .addOnProgressListener { up ->
@@ -46,6 +49,7 @@ class FirebaseNightlyProxy @Inject constructor(
                         when (e.errorCode) {
                             FirebaseAppDistributionException.Status.NOT_IMPLEMENTED -> {
                                 // SDK did nothing. This is expected when building for Play.
+                                Timber.d("FirebaseAppDistribution NOT_IMPLEMENTED error")
                             }
                             else -> {
                                 // Handle other errors.
@@ -56,10 +60,14 @@ class FirebaseNightlyProxy @Inject constructor(
                         Timber.e(e, "FirebaseAppDistribution - other error")
                     }
                 }
+                .addOnSuccessListener {
+                    Timber.d("FirebaseAppDistribution Success!")
+                }
     }
 
-    private fun canDisplayPopup(): Boolean {
-        if (buildMeta.applicationId != "im.vector.app.nightly") return false
+    override fun canDisplayPopup(): Boolean {
+        if (!POPUP_IS_ENABLED) return false
+        if (!isNightlyBuild()) return false
         val today = clock.epochMillis() / A_DAY_IN_MILLIS
         val lastDisplayPopupDay = sharedPreferences.getLong(SHARED_PREF_KEY, 0)
         return (today > lastDisplayPopupDay)
@@ -73,7 +81,12 @@ class FirebaseNightlyProxy @Inject constructor(
     }
 
     companion object {
+        private const val POPUP_IS_ENABLED = false
         private const val A_DAY_IN_MILLIS = 8_600_000L
         private const val SHARED_PREF_KEY = "LAST_NIGHTLY_POPUP_DAY"
+
+        private val nightlyPackages = listOf(
+                "im.vector.app.nightly"
+        )
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
index 8c6daae95a..2a3d8d094c 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
@@ -580,7 +580,9 @@ class HomeActivity :
         serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
 
         // Check nightly
-        nightlyProxy.onHomeResumed()
+        if (nightlyProxy.canDisplayPopup()) {
+            nightlyProxy.updateApplication()
+        }
 
         checkNewAppLayoutFlagChange()
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt b/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt
index b25add2ac9..42b93bf1a5 100644
--- a/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt
+++ b/vector/src/main/java/im/vector/app/features/home/NightlyProxy.kt
@@ -17,5 +17,18 @@
 package im.vector.app.features.home
 
 interface NightlyProxy {
-    fun onHomeResumed()
+    /**
+     * Return true if this is a nightly build (checking the package of the app), and only once a day.
+     */
+    fun canDisplayPopup(): Boolean
+
+    /**
+     * Return true if this is a nightly build (checking the package of the app).
+     */
+    fun isNightlyBuild(): Boolean
+
+    /**
+     * Try to update the application, if update is available. Will also take care of the user sign in.
+     */
+    fun updateApplication()
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
index 9c08d446f4..b6fa997f41 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedSettingsFragment.kt
@@ -22,10 +22,13 @@ import androidx.preference.SeekBarPreference
 import dagger.hilt.android.AndroidEntryPoint
 import im.vector.app.R
 import im.vector.app.core.platform.VectorBaseActivity
+import im.vector.app.core.preference.VectorPreference
 import im.vector.app.core.preference.VectorPreferenceCategory
 import im.vector.app.core.preference.VectorSwitchPreference
 import im.vector.app.features.analytics.plan.MobileScreen
+import im.vector.app.features.home.NightlyProxy
 import im.vector.app.features.rageshake.RageShake
+import javax.inject.Inject
 
 @AndroidEntryPoint
 class VectorSettingsAdvancedSettingsFragment :
@@ -34,6 +37,8 @@ class VectorSettingsAdvancedSettingsFragment :
     override var titleRes = R.string.settings_advanced_settings
     override val preferenceXmlRes = R.xml.vector_settings_advanced_settings
 
+    @Inject lateinit var nightlyProxy: NightlyProxy
+
     private var rageshake: RageShake? = null
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -57,6 +62,11 @@ class VectorSettingsAdvancedSettingsFragment :
     }
 
     override fun bindPref() {
+        setupRageShakeSection()
+        setupNightlySection()
+    }
+
+    private fun setupRageShakeSection() {
         val isRageShakeAvailable = RageShake.isAvailable(requireContext())
 
         if (isRageShakeAvailable) {
@@ -86,4 +96,12 @@ class VectorSettingsAdvancedSettingsFragment :
             findPreference("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false
         }
     }
+
+    private fun setupNightlySection() {
+        findPreference("SETTINGS_NIGHTLY_BUILD_PREFERENCE_KEY")?.isVisible = nightlyProxy.isNightlyBuild()
+        findPreference("SETTINGS_NIGHTLY_BUILD_UPDATE_PREFERENCE_KEY")?.setOnPreferenceClickListener {
+            nightlyProxy.updateApplication()
+            true
+        }
+    }
 }
diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
index 29d8051583..9260b33162 100644
--- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml
+++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml
@@ -95,4 +95,16 @@
 
     
 
+    
+
+        
+
+    
+
 

From ae93c075973b61d5a03b9842fa4bd1e0698bfb6f Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Tue, 6 Dec 2022 15:01:47 +0100
Subject: [PATCH 509/679] Do not propagate failure if saving the filter server
 side fails. This will be retried later.

---
 .../session/filter/GetCurrentFilterTask.kt     |  2 +-
 .../internal/session/filter/SaveFilterTask.kt  | 18 +++++++++++-------
 2 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
index e88f286e27..76805c5c51 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
@@ -42,7 +42,7 @@ internal class DefaultGetCurrentFilterTask @Inject constructor(
 
         return when (storedFilterBody) {
             currentFilterBody -> storedFilterId ?: storedFilterBody
-            else -> saveFilter(currentFilter)
+            else -> saveFilter(currentFilter) ?: currentFilterBody
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
index 82d5ff4d2f..0223cd3ee7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
@@ -24,8 +25,9 @@ import javax.inject.Inject
 
 /**
  * Save a filter, in db and if any changes, upload to the server.
+ * Return the filterId if uploading to the server is successful, else return null.
  */
-internal interface SaveFilterTask : Task {
+internal interface SaveFilterTask : Task {
 
     data class Params(
             val filter: Filter
@@ -39,18 +41,20 @@ internal class DefaultSaveFilterTask @Inject constructor(
         private val globalErrorReceiver: GlobalErrorReceiver,
 ) : SaveFilterTask {
 
-    override suspend fun execute(params: SaveFilterTask.Params): String {
+    override suspend fun execute(params: SaveFilterTask.Params): String? {
         val filter = params.filter
-        val filterResponse = executeRequest(globalErrorReceiver) {
-            // TODO auto retry
-            filterAPI.uploadFilter(userId, filter)
+        val filterResponse = tryOrNull {
+            executeRequest(globalErrorReceiver) {
+                filterAPI.uploadFilter(userId, filter)
+            }
         }
 
+        val filterId = filterResponse?.filterId
         filterRepository.storeSyncFilter(
                 filter = filter,
-                filterId = filterResponse.filterId,
+                filterId = filterId.orEmpty(),
                 roomEventFilter = FilterFactory.createDefaultRoomFilter()
         )
-        return filterResponse.filterId
+        return filterId
     }
 }

From 8646cc441d2c4c2d7e36c95f6dd3de71f5bd3dc3 Mon Sep 17 00:00:00 2001
From: valere 
Date: Tue, 6 Dec 2022 15:30:06 +0100
Subject: [PATCH 510/679] do not add tracing ids to verification events

---
 .../sdk/api/session/events/model/EventType.kt |  3 +
 .../internal/crypto/tasks/SendToDeviceTask.kt | 12 ++-
 .../crypto/DefaultSendToDeviceTaskTest.kt     | 97 ++++++++++++++++++-
 3 files changed, 109 insertions(+), 3 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index e5c14afa90..013b452ced 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.api.session.events.model
 
+import org.matrix.android.sdk.api.session.room.model.message.MessageType.MSGTYPE_VERIFICATION_REQUEST
+
 /**
  * Constants defining known event types from Matrix specifications.
  */
@@ -126,6 +128,7 @@ object EventType {
 
     fun isVerificationEvent(type: String): Boolean {
         return when (type) {
+            MSGTYPE_VERIFICATION_REQUEST,
             KEY_VERIFICATION_START,
             KEY_VERIFICATION_ACCEPT,
             KEY_VERIFICATION_KEY,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
index 1e6ceeb138..a7e93202ef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.tasks
 
 import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
 import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
@@ -39,7 +40,9 @@ internal interface SendToDeviceTask : Task {
             // the content to send. Map from user_id to device_id to content dictionary.
             val contentMap: MXUsersDevicesMap,
             // the transactionId. If not provided, a transactionId will be created by the task
-            val transactionId: String? = null
+            val transactionId: String? = null,
+            // add tracing id, notice that to device events that do signature on content might be broken by it
+            val addTracingIds: Boolean = !EventType.isVerificationEvent(eventType),
     )
 }
 
@@ -55,7 +58,12 @@ internal class DefaultSendToDeviceTask @Inject constructor(
         val txnId = params.transactionId ?: createUniqueTxnId()
 
         // add id tracing to debug
-        val decorated = decorateWithToDeviceTracingIds(params)
+        val decorated = if (params.addTracingIds) {
+            decorateWithToDeviceTracingIds(params)
+        } else {
+            params.contentMap.map to emptyList()
+        }
+
         val sendToDeviceBody = SendToDeviceBody(
                 messages = decorated.first
         )
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
index b8e870bd06..df6fc5f165 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
 import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
 import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
 import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
@@ -60,8 +61,28 @@ class DefaultSendToDeviceTaskTest {
             )
     )
 
+    private val fakeStartVerificationContent = mapOf(
+            "method" to "m.sas.v1",
+            "from_device" to "MNQHVEISFQ",
+            "key_agreement_protocols" to listOf(
+                    "curve25519-hkdf-sha256",
+                    "curve25519"
+            ),
+            "hashes" to listOf("sha256"),
+            "message_authentication_codes" to listOf(
+                    "org.matrix.msc3783.hkdf-hmac-sha256",
+                    "hkdf-hmac-sha256",
+                    "hmac-sha256"
+            ),
+            "short_authentication_string" to listOf(
+                    "decimal",
+                    "emoji"
+            ),
+            "transaction_id" to "4wNOpkHGwGZPXjkZToooCDWfb8hsf7vW"
+    )
+
     @Test
-    fun `tracing id should be added to all to_device contents`() {
+    fun `tracing id should be added to to_device contents`() {
         val fakeCryptoAPi = FakeCryptoApi()
 
         val sendToDeviceTask = DefaultSendToDeviceTask(
@@ -107,6 +128,80 @@ class DefaultSendToDeviceTaskTest {
         println("modified content ${fakeCryptoAPi.body}")
     }
 
+    @Test
+    fun `tracing id should not be added to verification start to_device contents`() {
+        val fakeCryptoAPi = FakeCryptoApi()
+
+        val sendToDeviceTask = DefaultSendToDeviceTask(
+                cryptoApi = fakeCryptoAPi,
+                globalErrorReceiver = mockk(relaxed = true)
+        )
+        val contentMap = MXUsersDevicesMap()
+        contentMap.setObject("@alice:example.com", "MNQHVEISFQ", fakeStartVerificationContent)
+
+        val params = SendToDeviceTask.Params(
+                eventType = EventType.KEY_VERIFICATION_START,
+                contentMap = contentMap
+        )
+
+        runBlocking {
+            sendToDeviceTask.execute(params)
+        }
+
+        val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
+        Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"])
+
+        // try to force
+        runBlocking {
+            sendToDeviceTask.execute(
+                    SendToDeviceTask.Params(
+                            eventType = EventType.KEY_VERIFICATION_START,
+                            contentMap = contentMap,
+                            addTracingIds = true
+                    )
+            )
+        }
+
+        val modifiedContentForced = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
+        Assert.assertNotNull("Tracing id should have been added", modifiedContentForced["org.matrix.msgid"])
+    }
+
+    @Test
+    fun `tracing id should not be added to all verification to_device contents`() {
+        val fakeCryptoAPi = FakeCryptoApi()
+
+        val sendToDeviceTask = DefaultSendToDeviceTask(
+                cryptoApi = fakeCryptoAPi,
+                globalErrorReceiver = mockk(relaxed = true)
+        )
+        val contentMap = MXUsersDevicesMap()
+        contentMap.setObject("@alice:example.com", "MNQHVEISFQ", emptyMap())
+
+        val verificationEvents = listOf(
+                MessageType.MSGTYPE_VERIFICATION_REQUEST,
+                EventType.KEY_VERIFICATION_START,
+                EventType.KEY_VERIFICATION_ACCEPT,
+                EventType.KEY_VERIFICATION_KEY,
+                EventType.KEY_VERIFICATION_MAC,
+                EventType.KEY_VERIFICATION_CANCEL,
+                EventType.KEY_VERIFICATION_DONE,
+                EventType.KEY_VERIFICATION_READY
+        )
+
+        for (type in verificationEvents) {
+            val params = SendToDeviceTask.Params(
+                    eventType = type,
+                    contentMap = contentMap
+            )
+            runBlocking {
+                sendToDeviceTask.execute(params)
+            }
+
+            val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
+            Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"])
+        }
+    }
+
     internal class FakeCryptoApi : CryptoApi {
         override suspend fun getDevices(): DevicesListResponse {
             throw java.lang.AssertionError("Should not be called")

From 63d2886415820d979c1e6592dd582123b1bb09ec Mon Sep 17 00:00:00 2001
From: valere 
Date: Tue, 6 Dec 2022 16:07:24 +0100
Subject: [PATCH 511/679] use msgid in logs for consistency

---
 .../sdk/internal/session/sync/handler/CryptoSyncHandler.kt    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
index 291e785aa5..551db52dbd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
@@ -49,14 +49,14 @@ internal class CryptoSyncHandler @Inject constructor(
                 ?.forEachIndexed { index, event ->
                     progressReporter?.reportProgress(index * 100F / total)
                     // Decrypt event if necessary
-                    Timber.tag(loggerTag.value).d("To device event tracingId:${event.toDeviceTracingId()}")
+                    Timber.tag(loggerTag.value).d("To device event msgid:${event.toDeviceTracingId()}")
                     decryptToDeviceEvent(event, null)
 
                     if (event.getClearType() == EventType.MESSAGE &&
                             event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") {
                         Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
                     } else {
-                        Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} id:${event.toDeviceTracingId()}")
+                        Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} msgid:${event.toDeviceTracingId()}")
                         verificationService.onToDeviceEvent(event)
                         cryptoService.onToDeviceEvent(event)
                     }

From a6752a0cf18fa974dd7ec5d7ba90954f21f1f17c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 6 Dec 2022 16:10:25 +0000
Subject: [PATCH 512/679] Bump com.google.devtools.ksp from 1.7.21-1.0.8 to
 1.7.22-1.0.8

Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 1.7.21-1.0.8 to 1.7.22-1.0.8.
- [Release notes](https://github.com/google/ksp/releases)
- [Commits](https://github.com/google/ksp/compare/1.7.21-1.0.8...1.7.22-1.0.8)

---
updated-dependencies:
- dependency-name: com.google.devtools.ksp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 58084ab64d..2abb2a9072 100644
--- a/build.gradle
+++ b/build.gradle
@@ -45,7 +45,7 @@ plugins {
     // Detekt
     id "io.gitlab.arturbosch.detekt" version "1.22.0"
     // Ksp
-    id "com.google.devtools.ksp" version "1.7.21-1.0.8"
+    id "com.google.devtools.ksp" version "1.7.22-1.0.8"
 
     // Dependency Analysis
     id 'com.autonomousapps.dependency-analysis' version "1.17.0"

From 988afa4ebe5354849972c5a8faa2a4a4a331c846 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Tue, 6 Dec 2022 18:21:07 +0100
Subject: [PATCH 513/679] Fix FDroid build

---
 vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt b/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt
index 63b4c2a3cd..e7d9598b65 100644
--- a/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt
+++ b/vector-app/src/fdroid/java/im/vector/app/di/FlavorModule.kt
@@ -46,9 +46,9 @@ abstract class FlavorModule {
 
         @Provides
         fun provideNightlyProxy() = object : NightlyProxy {
-            override fun onHomeResumed() {
-                // no op
-            }
+            override fun canDisplayPopup() = false
+            override fun isNightlyBuild() = false
+            override fun updateApplication() = Unit
         }
 
         @Provides

From 9bbecbeed33eba37af120ea004cd7c49fd539f21 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 6 Dec 2022 23:02:30 +0000
Subject: [PATCH 514/679] Bump wysiwyg from 0.8.0 to 0.9.0

Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.8.0 to 0.9.0.
- [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases)
- [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/0.8.0...0.9.0)

---
updated-dependencies:
- dependency-name: io.element.android:wysiwyg
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index fd630eba6d..b408ee01eb 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -98,7 +98,7 @@ ext.libs = [
         ],
         element     : [
                 'opusencoder'             : "io.element.android:opusencoder:1.1.0",
-                'wysiwyg'                 : "io.element.android:wysiwyg:0.8.0"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.9.0"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",

From 041fcef1db267de5f8dfcf954fb4f043d38238c0 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 7 Dec 2022 10:30:47 +0100
Subject: [PATCH 515/679] Adding changelog entry

---
 changelog.d/7733.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7733.bugfix

diff --git a/changelog.d/7733.bugfix b/changelog.d/7733.bugfix
new file mode 100644
index 0000000000..9de3759f1a
--- /dev/null
+++ b/changelog.d/7733.bugfix
@@ -0,0 +1 @@
+[Session manager] Sessions without encryption support should not prompt to verify

From 11dded71ec0c642039214c6deec0d73f8c1ca039 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 7 Dec 2022 13:54:20 +0100
Subject: [PATCH 516/679] Changelog

---
 changelog.d/7725.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7725.bugfix

diff --git a/changelog.d/7725.bugfix b/changelog.d/7725.bugfix
new file mode 100644
index 0000000000..b701451505
--- /dev/null
+++ b/changelog.d/7725.bugfix
@@ -0,0 +1 @@
+Fix crash when the network is not available.

From 1acd8e10499c6553996142591b17d44b9d375273 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Tue, 6 Dec 2022 15:01:47 +0100
Subject: [PATCH 517/679] Do not propagate failure if saving the filter server
 side fails. This will be retried later.

---
 .../session/filter/GetCurrentFilterTask.kt     |  2 +-
 .../internal/session/filter/SaveFilterTask.kt  | 18 +++++++++++-------
 2 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
index e88f286e27..76805c5c51 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
@@ -42,7 +42,7 @@ internal class DefaultGetCurrentFilterTask @Inject constructor(
 
         return when (storedFilterBody) {
             currentFilterBody -> storedFilterId ?: storedFilterBody
-            else -> saveFilter(currentFilter)
+            else -> saveFilter(currentFilter) ?: currentFilterBody
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
index 82d5ff4d2f..0223cd3ee7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
@@ -24,8 +25,9 @@ import javax.inject.Inject
 
 /**
  * Save a filter, in db and if any changes, upload to the server.
+ * Return the filterId if uploading to the server is successful, else return null.
  */
-internal interface SaveFilterTask : Task {
+internal interface SaveFilterTask : Task {
 
     data class Params(
             val filter: Filter
@@ -39,18 +41,20 @@ internal class DefaultSaveFilterTask @Inject constructor(
         private val globalErrorReceiver: GlobalErrorReceiver,
 ) : SaveFilterTask {
 
-    override suspend fun execute(params: SaveFilterTask.Params): String {
+    override suspend fun execute(params: SaveFilterTask.Params): String? {
         val filter = params.filter
-        val filterResponse = executeRequest(globalErrorReceiver) {
-            // TODO auto retry
-            filterAPI.uploadFilter(userId, filter)
+        val filterResponse = tryOrNull {
+            executeRequest(globalErrorReceiver) {
+                filterAPI.uploadFilter(userId, filter)
+            }
         }
 
+        val filterId = filterResponse?.filterId
         filterRepository.storeSyncFilter(
                 filter = filter,
-                filterId = filterResponse.filterId,
+                filterId = filterId.orEmpty(),
                 roomEventFilter = FilterFactory.createDefaultRoomFilter()
         )
-        return filterResponse.filterId
+        return filterId
     }
 }

From 53b703dcafe14b7153840bc72d52d789f60233cb Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 7 Dec 2022 13:54:20 +0100
Subject: [PATCH 518/679] Changelog

---
 changelog.d/7725.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7725.bugfix

diff --git a/changelog.d/7725.bugfix b/changelog.d/7725.bugfix
new file mode 100644
index 0000000000..b701451505
--- /dev/null
+++ b/changelog.d/7725.bugfix
@@ -0,0 +1 @@
+Fix crash when the network is not available.

From 6c84668611d2dd63a365caa27b9144646a6e582b Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 7 Dec 2022 13:58:02 +0100
Subject: [PATCH 519/679] Hotfix 1.5.11

---
 matrix-sdk-android/build.gradle | 2 +-
 vector-app/build.gradle         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 60b0329fbc..0b5dc1aacf 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -62,7 +62,7 @@ android {
         // that the app's state is completely cleared between tests.
         testInstrumentationRunnerArguments clearPackageData: 'true'
 
-        buildConfigField "String", "SDK_VERSION", "\"1.5.10\""
+        buildConfigField "String", "SDK_VERSION", "\"1.5.11\""
 
         buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
         buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index 86b94a8497..0796afe38b 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -37,7 +37,7 @@ ext.versionMinor = 5
 // Note: even values are reserved for regular release, odd values for hotfix release.
 // When creating a hotfix, you should decrease the value, since the current value
 // is the value for the next regular release.
-ext.versionPatch = 10
+ext.versionPatch = 11
 
 static def getGitTimestamp() {
     def cmd = 'git show -s --format=%ct'

From a0bba91d67f8832812e766baf31b527a5acd6e42 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 7 Dec 2022 13:58:51 +0100
Subject: [PATCH 520/679] Fastlane file for hotfix 1.5.11

---
 fastlane/metadata/android/en-US/changelogs/40105110.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/en-US/changelogs/40105110.txt

diff --git a/fastlane/metadata/android/en-US/changelogs/40105110.txt b/fastlane/metadata/android/en-US/changelogs/40105110.txt
new file mode 100644
index 0000000000..c9e5ba5fa9
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105110.txt
@@ -0,0 +1,2 @@
+Main changes in this version: New implementation of the full screen mode for the Rich Text Editor and bugfixes.
+Full changelog: https://github.com/vector-im/element-android/releases

From 3132a7d463d7ce4443a2d1496c3452a85706e95c Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 7 Dec 2022 13:59:42 +0100
Subject: [PATCH 521/679] Towncrier 1.5.11

---
 CHANGES.md              | 8 ++++++++
 changelog.d/7725.bugfix | 1 -
 2 files changed, 8 insertions(+), 1 deletion(-)
 delete mode 100644 changelog.d/7725.bugfix

diff --git a/CHANGES.md b/CHANGES.md
index 022591f5a1..c170c3b92b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,11 @@
+Changes in Element 1.5.11 (2022-12-07)
+======================================
+
+Bugfixes 🐛
+----------
+ - Fix crash when the network is not available. ([#7725](https://github.com/vector-im/element-android/issues/7725))
+
+
 Changes in Element v1.5.10 (2022-11-30)
 =======================================
 
diff --git a/changelog.d/7725.bugfix b/changelog.d/7725.bugfix
deleted file mode 100644
index b701451505..0000000000
--- a/changelog.d/7725.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Fix crash when the network is not available.

From c9c5483d227e80d3761596fecba6502ea000321c Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 7 Dec 2022 14:09:59 +0100
Subject: [PATCH 522/679] Changelog

---
 changelog.d/7723.misc | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7723.misc

diff --git a/changelog.d/7723.misc b/changelog.d/7723.misc
new file mode 100644
index 0000000000..36869d1efb
--- /dev/null
+++ b/changelog.d/7723.misc
@@ -0,0 +1 @@
+Disable nightly popup and add an entry point in the advanced settings instead.

From f014866d063a6c7d1df769c4c74cbd6aa7d916c9 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 7 Dec 2022 14:34:45 +0100
Subject: [PATCH 523/679] Handling the case where device has no
 CryptoDeviceInfo

---
 .../src/main/res/values/strings.xml           |  2 +
 .../app/core/ui/views/ShieldImageView.kt      | 28 ++++++++------
 .../settings/devices/DevicesViewModel.kt      |  2 +-
 .../settings/devices/v2/DeviceFullInfo.kt     |  2 +-
 .../devices/v2/list/SessionInfoView.kt        | 11 +++++-
 .../v2/overview/SessionOverviewFragment.kt    | 37 +++++++++++--------
 ...GetEncryptionTrustLevelForDeviceUseCase.kt | 10 +++--
 ...ncryptionTrustLevelForDeviceUseCaseTest.kt | 15 ++++++++
 8 files changed, 75 insertions(+), 32 deletions(-)

diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 609cdac233..b10d11d048 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3305,6 +3305,7 @@
     Verify your current session for enhanced secure messaging.
     Verify or sign out from this session for best security and reliability.
     Verify your current session to reveal this session\'s verification status.
+    This session doesn\'t support encryption and thus can\'t be verified.
     Verify Session
     View Details
     View All (%1$d)
@@ -3397,6 +3398,7 @@
     
     Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you.
     Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.
+    This session doesn\'t support encryption, so it can\'t be verified.\n\nYou won\'t be able to participate in rooms where encryption is enabled when using this session.\n\nFor best security and privacy, it is recommended to use Matrix clients that support encryption.
     Renaming sessions
     Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.
     Enable new session manager
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
index 0570bbe4d7..34714d97d0 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt
@@ -40,20 +40,26 @@ class ShieldImageView @JvmOverloads constructor(
 
     /**
      * Renders device shield with the support of unknown shields instead of black shields which is used for rooms.
-     * @param roomEncryptionTrustLevel trust level that is usally calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust]
+     * @param roomEncryptionTrustLevel trust level that is usually calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust]
      * @param borderLess if true then the shield icon with border around is used
      */
     fun renderDeviceShield(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
-        isVisible = roomEncryptionTrustLevel != null
-
-        if (roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Default) {
-            contentDescription = context.getString(R.string.a11y_trust_level_default)
-            setImageResource(
-                    if (borderLess) R.drawable.ic_shield_unknown_no_border
-                    else R.drawable.ic_shield_unknown
-            )
-        } else {
-            render(roomEncryptionTrustLevel, borderLess)
+        when (roomEncryptionTrustLevel) {
+            null -> {
+                contentDescription = context.getString(R.string.a11y_trust_level_warning)
+                setImageResource(
+                        if (borderLess) R.drawable.ic_shield_warning_no_border
+                        else R.drawable.ic_shield_warning
+                )
+            }
+            RoomEncryptionTrustLevel.Default -> {
+                contentDescription = context.getString(R.string.a11y_trust_level_default)
+                setImageResource(
+                        if (borderLess) R.drawable.ic_shield_unknown_no_border
+                        else R.drawable.ic_shield_unknown
+                )
+            }
+            else -> render(roomEncryptionTrustLevel, borderLess)
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
index 67b41ea5aa..e779948b41 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
@@ -88,7 +88,7 @@ data class DevicesViewState(
 data class DeviceFullInfo(
         val deviceInfo: DeviceInfo,
         val cryptoDeviceInfo: CryptoDeviceInfo?,
-        val trustLevelForShield: RoomEncryptionTrustLevel,
+        val trustLevelForShield: RoomEncryptionTrustLevel?,
         val isInactive: Boolean,
 )
 
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt
index 4864c41394..186a6ebe69 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt
@@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 data class DeviceFullInfo(
         val deviceInfo: DeviceInfo,
         val cryptoDeviceInfo: CryptoDeviceInfo?,
-        val roomEncryptionTrustLevel: RoomEncryptionTrustLevel,
+        val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?,
         val isInactive: Boolean,
         val isCurrentDevice: Boolean,
         val deviceExtendedInfo: DeviceExtendedInfo,
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
index 7727cee4fa..eecec72b0a 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
@@ -85,13 +85,14 @@ class SessionInfoView @JvmOverloads constructor(
     }
 
     private fun renderVerificationStatus(
-            encryptionTrustLevel: RoomEncryptionTrustLevel,
+            encryptionTrustLevel: RoomEncryptionTrustLevel?,
             isCurrentSession: Boolean,
             hasLearnMoreLink: Boolean,
             isVerifyButtonVisible: Boolean,
     ) {
         views.sessionInfoVerificationStatusImageView.renderDeviceShield(encryptionTrustLevel)
         when {
+            encryptionTrustLevel == null -> renderCrossSigningEncryptionNotSupported()
             encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession)
             encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown()
             else -> renderCrossSigningUnverified(isCurrentSession, isVerifyButtonVisible)
@@ -149,6 +150,14 @@ class SessionInfoView @JvmOverloads constructor(
         views.sessionInfoVerifySessionButton.isVisible = false
     }
 
+    private fun renderCrossSigningEncryptionNotSupported() {
+        views.sessionInfoVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_unverified)
+        views.sessionInfoVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorError))
+        views.sessionInfoVerificationStatusDetailTextView.text =
+                context.getString(R.string.device_manager_verification_status_detail_session_encryption_not_supported)
+        views.sessionInfoVerifySessionButton.isVisible = false
+    }
+
     private fun renderDeviceInfo(sessionName: String, deviceType: DeviceType, stringProvider: StringProvider) {
         setDeviceTypeIconUseCase.execute(deviceType, views.sessionInfoDeviceTypeImageView, stringProvider)
         views.sessionInfoNameTextView.text = sessionName
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
index be60b3b805..6fbd2f1fef 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
@@ -229,7 +229,7 @@ class SessionOverviewFragment :
             )
             views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
             views.sessionOverviewInfo.onLearnMoreClickListener = {
-                showLearnMoreInfoVerificationStatus(deviceInfo.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted)
+                showLearnMoreInfoVerificationStatus(deviceInfo.roomEncryptionTrustLevel)
             }
         } else {
             views.sessionOverviewInfo.isVisible = false
@@ -293,21 +293,28 @@ class SessionOverviewFragment :
         }
     }
 
-    private fun showLearnMoreInfoVerificationStatus(isVerified: Boolean) {
-        val titleResId = if (isVerified) {
-            R.string.device_manager_verification_status_verified
-        } else {
-            R.string.device_manager_verification_status_unverified
-        }
-        val descriptionResId = if (isVerified) {
-            R.string.device_manager_learn_more_sessions_verified_description
-        } else {
-            R.string.device_manager_learn_more_sessions_unverified
+    private fun showLearnMoreInfoVerificationStatus(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?) {
+        val args = when(roomEncryptionTrustLevel) {
+            null -> {
+                // encryption not supported
+                SessionLearnMoreBottomSheet.Args(
+                        title = getString(R.string.device_manager_verification_status_unverified),
+                        description = getString(R.string.device_manager_learn_more_sessions_encryption_not_supported),
+                )
+            }
+            RoomEncryptionTrustLevel.Trusted -> {
+                SessionLearnMoreBottomSheet.Args(
+                        title = getString(R.string.device_manager_verification_status_verified),
+                        description = getString(R.string.device_manager_learn_more_sessions_verified_description),
+                )
+            }
+            else -> {
+                SessionLearnMoreBottomSheet.Args(
+                        title = getString(R.string.device_manager_verification_status_unverified),
+                        description = getString(R.string.device_manager_learn_more_sessions_unverified),
+                )
+            }
         }
-        val args = SessionLearnMoreBottomSheet.Args(
-                title = getString(titleResId),
-                description = getString(descriptionResId),
-        )
         SessionLearnMoreBottomSheet.show(childFragmentManager, args)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt
index ba9a380ade..31a7e93d04 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt
@@ -25,11 +25,15 @@ class GetEncryptionTrustLevelForDeviceUseCase @Inject constructor(
         private val getEncryptionTrustLevelForOtherDeviceUseCase: GetEncryptionTrustLevelForOtherDeviceUseCase,
 ) {
 
-    fun execute(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel {
+    fun execute(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel? {
+        if(cryptoDeviceInfo == null) {
+            return null
+        }
+
         val legacyMode = !currentSessionCrossSigningInfo.isCrossSigningInitialized
         val trustMSK = currentSessionCrossSigningInfo.isCrossSigningVerified
-        val isCurrentDevice = !cryptoDeviceInfo?.deviceId.isNullOrEmpty() && cryptoDeviceInfo?.deviceId == currentSessionCrossSigningInfo.deviceId
-        val deviceTrustLevel = cryptoDeviceInfo?.trustLevel
+        val isCurrentDevice = !cryptoDeviceInfo.deviceId.isNullOrEmpty() && cryptoDeviceInfo.deviceId == currentSessionCrossSigningInfo.deviceId
+        val deviceTrustLevel = cryptoDeviceInfo.trustLevel
 
         return when {
             isCurrentDevice -> getEncryptionTrustLevelForCurrentDeviceUseCase.execute(trustMSK, legacyMode)
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt
index 1b39fe5f73..fd10ee1083 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCaseTest.kt
@@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2.verification
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.verify
+import org.amshove.kluent.shouldBe
 import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Test
 import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
@@ -89,6 +90,20 @@ class GetEncryptionTrustLevelForDeviceUseCaseTest {
         }
     }
 
+    @Test
+    fun `given no crypto device info when computing trust level then result is null`() {
+        val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo(
+                deviceId = A_DEVICE_ID,
+                isCrossSigningInitialized = true,
+                isCrossSigningVerified = false
+        )
+        val cryptoDeviceInfo = null
+
+        val result = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
+
+        result shouldBe null
+    }
+
     private fun givenCurrentSessionCrossSigningInfo(
             deviceId: String,
             isCrossSigningInitialized: Boolean,

From a44c8dfca302bab2e50f349acdbf0ed4a419a325 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 7 Dec 2022 15:10:21 +0100
Subject: [PATCH 524/679] Renaming a method to avoid confusion

---
 .../settings/devices/v2/VectorSettingsDevicesFragment.kt      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index a21c7accb7..c21b044f1f 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -102,7 +102,7 @@ class VectorSettingsDevicesFragment :
 
         initWaitingView()
         initCurrentSessionHeaderView()
-        initCurrentSessionListView()
+        initCurrentSessionView()
         initOtherSessionsHeaderView()
         initOtherSessionsView()
         initSecurityRecommendationsView()
@@ -177,7 +177,7 @@ class VectorSettingsDevicesFragment :
         activity?.let { SignOutUiWorker(it).perform() }
     }
 
-    private fun initCurrentSessionListView() {
+    private fun initCurrentSessionView() {
         views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
             viewModel.handle(DevicesAction.VerifyCurrentSession)
         }

From 6c94f1cd52a6e871281a299ef92614dfeddb5cf2 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Wed, 7 Dec 2022 15:50:26 +0100
Subject: [PATCH 525/679] Quick tweak on the release script.

---
 tools/release/releaseScript.sh | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index d76cd98061..f91e11584c 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -345,7 +345,8 @@ ${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signe
 printf "File vector-gplay-x86_64-release-signed.apk:\n"
 ${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package
 
-read -p "\nDoes it look correct? Press enter when it's done."
+printf "\n"
+read -p "Does it look correct? Press enter when it's done."
 
 printf "\n================================================================================\n"
 read -p "Installing apk on a real device, press enter when a real device is connected. "
@@ -356,7 +357,7 @@ read -p "Please run the APK on your phone to check that the upgrade went well (n
 # TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)?
 read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done."
 
-read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done."
+read -p "Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}. Press enter when it's done."
 
 printf "\n================================================================================\n"
 printf "Message for the Android internal room:\n\n"

From 88f743988064b51c1562b8c9291032f2e83639a8 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 7 Dec 2022 15:52:56 +0100
Subject: [PATCH 526/679] Updating comment to clarify intention

---
 .../app/features/home/UnknownDeviceDetectorSharedViewModel.kt   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
index 21c7bd6ea1..347c16653d 100644
--- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
@@ -104,7 +104,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
 //                    Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
             infoList
                     .filter { info ->
-                        // filter verified session, by checking the crypto device info
+                        // filter out verified sessions or those which do not support encryption (i.e. without crypto info)
                         cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
                     }
                     // filter out ignored devices

From d244f7324c40c59b82c315491ad459591f006d81 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Wed, 7 Dec 2022 18:12:25 +0300
Subject: [PATCH 527/679] Add api functions to delete account data.

---
 .../android/sdk/internal/session/room/RoomAPI.kt    | 13 +++++++++++++
 .../session/user/accountdata/AccountDataAPI.kt      | 13 +++++++++++++
 2 files changed, 26 insertions(+)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 31bed90b62..3cf5526a47 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -427,6 +427,19 @@ internal interface RoomAPI {
             @Body content: JsonDict
     )
 
+    /**
+     * Remove an account_data event from the room.
+     * @param userId the user id
+     * @param roomId the room id
+     * @param type the type
+     */
+    @DELETE(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}")
+    suspend fun deleteRoomAccountData(
+            @Path("userId") userId: String,
+            @Path("roomId") roomId: String,
+            @Path("type") type: String
+    )
+
     /**
      * Upgrades the given room to a particular room version.
      * Errors:
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
index b283d51845..fd813f1fed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.user.accountdata
 
 import org.matrix.android.sdk.internal.network.NetworkConstants
 import retrofit2.http.Body
+import retrofit2.http.DELETE
 import retrofit2.http.PUT
 import retrofit2.http.Path
 
@@ -36,4 +37,16 @@ internal interface AccountDataAPI {
             @Path("type") type: String,
             @Body params: Any
     )
+
+    /**
+     * Remove an account_data for the client.
+     *
+     * @param userId the user id
+     * @param type the type
+     */
+    @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/account_data/{type}")
+    suspend fun deleteAccountData(
+            @Path("userId") userId: String,
+            @Path("type") type: String
+    )
 }

From 765202e05a841c257b771bb0d446e61f4f368b3f Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Wed, 7 Dec 2022 18:17:43 +0300
Subject: [PATCH 528/679] Add helper functions to delete user and room account
 data.

---
 .../database/model/RoomAccountDataEntity.kt   |  5 ++-
 .../query/RoomAccountDataEntityQueries.kt     | 33 +++++++++++++++++++
 .../query/UserAccountDataEntityQueries.kt     | 33 +++++++++++++++++++
 3 files changed, 70 insertions(+), 1 deletion(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt
index 40040b5738..2eb5a63784 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt
@@ -24,4 +24,7 @@ import io.realm.annotations.RealmClass
 internal open class RoomAccountDataEntity(
         @Index var type: String? = null,
         var contentStr: String? = null
-) : RealmObject()
+) : RealmObject() {
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt
new file mode 100644
index 0000000000..5ae4c5da72
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.query
+
+import io.realm.Realm
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity
+import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
+
+/**
+ * Delete an account_data event.
+ */
+internal fun RoomAccountDataEntity.Companion.delete(realm: Realm, type: String) {
+    realm
+            .where()
+            .equalTo(RoomAccountDataEntityFields.TYPE, type)
+            .findFirst()
+            ?.deleteFromRealm()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt
new file mode 100644
index 0000000000..b28965aeca
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.query
+
+import io.realm.Realm
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
+import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
+
+/**
+ * Delete an account_data event.
+ */
+internal fun UserAccountDataEntity.Companion.delete(realm: Realm, type: String) {
+    realm
+            .where()
+            .equalTo(UserAccountDataEntityFields.TYPE, type)
+            .findFirst()
+            ?.deleteFromRealm()
+}

From 23c2682f8d4e85466a90352f99bb9ad2b40630c0 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Wed, 7 Dec 2022 16:39:51 +0100
Subject: [PATCH 529/679] Fixing code style issues

---
 .../settings/devices/v2/overview/SessionOverviewFragment.kt     | 2 +-
 .../v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
index 6fbd2f1fef..f3df0cced0 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
@@ -294,7 +294,7 @@ class SessionOverviewFragment :
     }
 
     private fun showLearnMoreInfoVerificationStatus(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?) {
-        val args = when(roomEncryptionTrustLevel) {
+        val args = when (roomEncryptionTrustLevel) {
             null -> {
                 // encryption not supported
                 SessionLearnMoreBottomSheet.Args(
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt
index 31a7e93d04..268ae86601 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/GetEncryptionTrustLevelForDeviceUseCase.kt
@@ -26,7 +26,7 @@ class GetEncryptionTrustLevelForDeviceUseCase @Inject constructor(
 ) {
 
     fun execute(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel? {
-        if(cryptoDeviceInfo == null) {
+        if (cryptoDeviceInfo == null) {
             return null
         }
 

From f4429d4c9c9acdc4495e9e3fde098688626fe5f0 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Wed, 7 Dec 2022 18:58:14 +0300
Subject: [PATCH 530/679] Handle sync response to delete user and room account
 data.

---
 .../query/RoomAccountDataEntityQueries.kt     | 33 -------------------
 .../database/query/RoomEntityQueries.kt       |  9 +++++
 .../handler/UserAccountDataSyncHandler.kt     | 21 +++++++-----
 .../parsing/RoomSyncAccountDataHandler.kt     | 19 +++++++----
 4 files changed, 34 insertions(+), 48 deletions(-)
 delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt
deleted file mode 100644
index 5ae4c5da72..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomAccountDataEntityQueries.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 2022 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.matrix.android.sdk.internal.database.query
-
-import io.realm.Realm
-import io.realm.kotlin.where
-import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity
-import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
-
-/**
- * Delete an account_data event.
- */
-internal fun RoomAccountDataEntity.Companion.delete(realm: Realm, type: String) {
-    realm
-            .where()
-            .equalTo(RoomAccountDataEntityFields.TYPE, type)
-            .findFirst()
-            ?.deleteFromRealm()
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt
index 08bb9e7ff3..0489fe690f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt
@@ -21,6 +21,7 @@ import io.realm.RealmQuery
 import io.realm.kotlin.where
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 
@@ -44,3 +45,11 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? =
 internal fun RoomEntity.fastContains(eventId: String): Boolean {
     return EventEntity.where(realm, eventId = eventId).findFirst() != null
 }
+
+internal fun RoomEntity.removeAccountData(type: String) {
+    accountData
+            .where()
+            .equalTo(RoomAccountDataEntityFields.TYPE, type)
+            .findFirst()
+            ?.deleteFromRealm()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
index 0f296ded5d..fb2dfa10f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
@@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
 import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
 import org.matrix.android.sdk.internal.database.model.deleteOnCascade
+import org.matrix.android.sdk.internal.database.query.delete
 import org.matrix.android.sdk.internal.database.query.findAllFrom
 import org.matrix.android.sdk.internal.database.query.getDirectRooms
 import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -81,20 +82,24 @@ internal class UserAccountDataSyncHandler @Inject constructor(
 
     fun handle(realm: Realm, accountData: UserAccountDataSync?) {
         accountData?.list?.forEach { event ->
-            // Generic handling, just save in base
-            handleGenericAccountData(realm, event.type, event.content)
-            when (event.type) {
-                UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event)
-                UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event)
-                UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event)
-                UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event)
+            if (event.content.isEmpty()) {
+                UserAccountDataEntity.delete(realm, event.type)
+            } else {
+                // Generic handling, just save in base
+                handleGenericAccountData(realm, event.type, event.content)
+                when (event.type) {
+                    UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event)
+                    UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event)
+                    UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event)
+                    UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event)
+                }
             }
         }
     }
 
     // If we get some direct chat invites, we synchronize the user account data including those.
     suspend fun synchronizeWithServerIfNeeded(invites: Map) {
-        if (invites.isNullOrEmpty()) return
+        if (invites.isEmpty()) return
         val directChats = directChatsHelper.getLocalDirectMessages().toMutable()
         var hasUpdate = false
         monarchy.doWithRealm { realm ->
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
index b1b2bfef33..c5f8294077 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
@@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity
 import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.database.query.removeAccountData
 import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
 import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler
@@ -44,13 +45,17 @@ internal class RoomSyncAccountDataHandler @Inject constructor(
         val roomEntity = RoomEntity.getOrCreate(realm, roomId)
         for (event in accountData.events) {
             val eventType = event.getClearType()
-            handleGeneric(roomEntity, event.getClearContent(), eventType)
-            if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) {
-                val content = event.getClearContent().toModel()
-                roomTagHandler.handle(realm, roomId, content)
-            } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) {
-                val content = event.getClearContent().toModel()
-                roomFullyReadHandler.handle(realm, roomId, content)
+            if (event.getClearContent().isNullOrEmpty()) {
+                roomEntity.removeAccountData(eventType)
+            } else {
+                handleGeneric(roomEntity, event.getClearContent(), eventType)
+                if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) {
+                    val content = event.getClearContent().toModel()
+                    roomTagHandler.handle(realm, roomId, content)
+                } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) {
+                    val content = event.getClearContent().toModel()
+                    roomFullyReadHandler.handle(realm, roomId, content)
+                }
             }
         }
     }

From fdb8743ad36896dfef6760ae3d7402780dabe538 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 1 Dec 2022 16:14:21 +0100
Subject: [PATCH 531/679] Create provider package

---
 .../android/sdk/common/TestRoomDisplayNameFallbackProvider.kt   | 2 +-
 .../main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt | 2 ++
 .../api/{ => provider}/MatrixItemDisplayNameFallbackProvider.kt | 2 +-
 .../sdk/api/{ => provider}/RoomDisplayNameFallbackProvider.kt   | 2 +-
 .../displayname/VectorMatrixItemDisplayNameFallbackProvider.kt  | 2 +-
 .../app/features/room/VectorRoomDisplayNameFallbackProvider.kt  | 2 +-
 6 files changed, 7 insertions(+), 5 deletions(-)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/{ => provider}/MatrixItemDisplayNameFallbackProvider.kt (94%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/{ => provider}/RoomDisplayNameFallbackProvider.kt (97%)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt
index af2d57f9ce..a74f5010c2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.common
 
-import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider
+import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
 
 class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
index 00d74ab446..d19fbe5049 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
@@ -20,6 +20,8 @@ import okhttp3.ConnectionSpec
 import okhttp3.Interceptor
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import org.matrix.android.sdk.api.metrics.MetricPlugin
+import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider
+import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
 import java.net.Proxy
 
 data class MatrixConfiguration(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt
index 82008cda8c..971845eae7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixItemDisplayNameFallbackProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/MatrixItemDisplayNameFallbackProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.api
+package org.matrix.android.sdk.api.provider
 
 import org.matrix.android.sdk.api.util.MatrixItem
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt
similarity index 97%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt
index 3c376b55ee..37d9b46b0b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/RoomDisplayNameFallbackProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.api
+package org.matrix.android.sdk.api.provider
 
 /**
  * This interface exists to let the implementation provide localized room display name fallback.
diff --git a/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt
index 23b55335b8..77f08bf578 100644
--- a/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/displayname/VectorMatrixItemDisplayNameFallbackProvider.kt
@@ -16,7 +16,7 @@
 
 package im.vector.app.features.displayname
 
-import org.matrix.android.sdk.api.MatrixItemDisplayNameFallbackProvider
+import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider
 import org.matrix.android.sdk.api.util.MatrixItem
 
 // Used to provide the fallback to the MatrixSDK, in the MatrixConfiguration
diff --git a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt
index 118017861c..cfbc2748ad 100644
--- a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt
+++ b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt
@@ -18,7 +18,7 @@ package im.vector.app.features.room
 
 import android.content.Context
 import im.vector.app.R
-import org.matrix.android.sdk.api.RoomDisplayNameFallbackProvider
+import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
 import javax.inject.Inject
 
 class VectorRoomDisplayNameFallbackProvider @Inject constructor(

From 4d6c04baf9d713997b1061e6f9d94fe34d0ff00d Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 1 Dec 2022 18:08:30 +0100
Subject: [PATCH 532/679] Add provider for custom event types

---
 .../android/sdk/api/MatrixConfiguration.kt    |  8 +++--
 .../api/provider/CustomEventTypesProvider.kt  | 30 +++++++++++++++++++
 .../room/summary/RoomSummaryEventsHelper.kt   | 10 +++++--
 .../room/summary/RoomSummaryUpdater.kt        |  7 +++--
 4 files changed, 48 insertions(+), 7 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
index d19fbe5049..ccfe557ef6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
@@ -20,6 +20,7 @@ import okhttp3.ConnectionSpec
 import okhttp3.Interceptor
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import org.matrix.android.sdk.api.metrics.MetricPlugin
+import org.matrix.android.sdk.api.provider.CustomEventTypesProvider
 import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider
 import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider
 import java.net.Proxy
@@ -77,9 +78,12 @@ data class MatrixConfiguration(
          * Sync configuration.
          */
         val syncConfig: SyncConfig = SyncConfig(),
-
         /**
          * Metrics plugin that can be used to capture metrics from matrix-sdk-android.
          */
-        val metricPlugins: List = emptyList()
+        val metricPlugins: List = emptyList(),
+        /**
+         * CustomEventTypesProvider to provide custom event types to the sdk which should be processed with internal events.
+         */
+        val customEventTypesProvider: CustomEventTypesProvider? = null,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt
new file mode 100644
index 0000000000..c0f66dc1c2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/CustomEventTypesProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.provider
+
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+
+/**
+ * Provide custom event types which should be processed with the internal event types.
+ */
+interface CustomEventTypesProvider {
+
+    /**
+     * Custom event types to include when computing [RoomSummary.latestPreviewableEvent].
+     */
+    val customPreviewableEventTypes: List
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt
index 7437a686da..a68ae620dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt
@@ -17,17 +17,23 @@
 package org.matrix.android.sdk.internal.session.room.summary
 
 import io.realm.Realm
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants
 import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.latestEvent
+import javax.inject.Inject
 
-internal object RoomSummaryEventsHelper {
+internal class RoomSummaryEventsHelper @Inject constructor(
+        matrixConfiguration: MatrixConfiguration,
+) {
 
     private val previewFilters = TimelineEventFilters(
             filterTypes = true,
-            allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES.map { EventTypeFilter(eventType = it, stateKey = null) },
+            allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES
+                    .plus(matrixConfiguration.customEventTypesProvider?.customPreviewableEventTypes.orEmpty())
+                    .map { EventTypeFilter(eventType = it, stateKey = null) },
             filterUseless = true,
             filterRedacted = false,
             filterEdits = true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index 21a0862c65..69beb8d599 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -78,12 +78,13 @@ internal class RoomSummaryUpdater @Inject constructor(
         private val crossSigningService: DefaultCrossSigningService,
         private val roomAccountDataDataSource: RoomAccountDataDataSource,
         private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
+        private val roomSummaryEventsHelper: RoomSummaryEventsHelper,
 ) {
 
     fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
         val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
         if (roomSummaryEntity != null) {
-            val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
+            val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
             latestPreviewableEvent?.attemptToDecrypt()
         }
     }
@@ -145,7 +146,7 @@ internal class RoomSummaryUpdater @Inject constructor(
         val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
         Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
 
-        val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
+        val latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
 
         val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs
         if (lastActivityFromEvent != null) {
@@ -231,7 +232,7 @@ internal class RoomSummaryUpdater @Inject constructor(
     fun updateSendingInformation(realm: Realm, roomId: String) {
         val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
         roomSummaryEntity.updateHasFailedSending()
-        roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
+        roomSummaryEntity.latestPreviewableEvent = roomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
     }
 
     /**

From 6e5461f300d4724aa853cfa4ee849f61262b80cb Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 2 Dec 2022 17:24:40 +0100
Subject: [PATCH 533/679] Stop filtering events with reference relationship
 when computing latest previewable event

---
 .../sdk/internal/database/query/TimelineEventEntityQueries.kt    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 1b4b359916..37df901c7d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -115,7 +115,6 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
     if (filters.filterEdits) {
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
-        not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
     }
     if (filters.filterRedacted) {
         not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)

From 1a3ca7b1a06b943fe9a36e53f7e5cee3d145cdff Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Fri, 2 Dec 2022 17:25:54 +0100
Subject: [PATCH 534/679] Filter event types from decrypted content

---
 .../query/TimelineEventEntityQueries.kt       | 43 ++++++++++++++++---
 .../database/query/TimelineEventFilter.kt     |  1 +
 2 files changed, 37 insertions(+), 7 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 37df901c7d..ab90801b7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -22,6 +22,7 @@ import io.realm.RealmQuery
 import io.realm.RealmResults
 import io.realm.Sort
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -94,14 +95,27 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
     if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) {
         beginGroup()
         filters.allowedTypes.forEachIndexed { index, filter ->
-            if (filter.stateKey == null) {
-                equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
+            if (filter.eventType == EventType.ENCRYPTED) {
+                val otherTypes = filters.allowedTypes.minus(filter).map { it.eventType }
+                if (filter.stateKey == null) {
+                    filterEncryptedTypes(otherTypes)
+                } else {
+                    beginGroup()
+                    filterEncryptedTypes(otherTypes)
+                    and()
+                    equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
+                    endGroup()
+                }
             } else {
-                beginGroup()
-                equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
-                and()
-                equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
-                endGroup()
+                if (filter.stateKey == null) {
+                    equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
+                } else {
+                    beginGroup()
+                    equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
+                    and()
+                    equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
+                    endGroup()
+                }
             }
             if (index != filters.allowedTypes.size - 1) {
                 or()
@@ -123,6 +137,21 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
     return this
 }
 
+internal fun RealmQuery.filterEncryptedTypes(allowedTypes: List): RealmQuery {
+    beginGroup()
+    equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.ENCRYPTED)
+    and()
+    beginGroup()
+    isNull(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON)
+    allowedTypes.forEach { eventType ->
+        or()
+        like(TimelineEventEntityFields.ROOT.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.type(eventType))
+    }
+    endGroup()
+    endGroup()
+    return this
+}
+
 internal fun RealmQuery.filterTypes(filterTypes: List): RealmQuery {
     return if (filterTypes.isEmpty()) {
         this
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
index 7a65623b76..b8baeb0b33 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
@@ -34,6 +34,7 @@ internal object TimelineEventFilter {
      */
     internal object DecryptedContent {
         internal const val URL = """{*"file":*"url":*}"""
+        fun type(type: String) = """{*"type":*"$type"*}"""
     }
 
     /**

From 69beef464871e77cccb41ec0928da109262d5278 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Thu, 1 Dec 2022 18:15:14 +0100
Subject: [PATCH 535/679] Show voice broadcast events in the room list

fix factory
---
 .../src/main/res/values/strings.xml           |  3 ++
 .../im/vector/app/core/di/SingletonModule.kt  |  3 ++
 .../VectorCustomEventTypesProvider.kt         | 28 +++++++++++++++++++
 .../format/DisplayableEventFormatter.kt       | 26 +++++++++++++++++
 .../home/room/list/RoomSummaryItemFactory.kt  | 23 +++++++++++++--
 .../GetOngoingVoiceBroadcastsUseCase.kt       |  4 +--
 6 files changed, 83 insertions(+), 4 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt

diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 7f11e63469..2d289150c6 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -134,6 +134,8 @@
     ** Unable to decrypt: %s **
     The sender\'s device has not sent us the keys for this message.
 
+    %1$s ended a voice broadcast.
+
     
 
     
@@ -3101,6 +3103,7 @@
     (%1$s)
 
     Live
+    Live broadcast
     
     Buffering…
     Resume voice broadcast record
diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt
index 28ca761ace..7a3aa7cf8b 100644
--- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt
+++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt
@@ -48,6 +48,7 @@ import im.vector.app.features.analytics.AnalyticsTracker
 import im.vector.app.features.analytics.VectorAnalytics
 import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
 import im.vector.app.features.analytics.metrics.VectorPlugins
+import im.vector.app.features.configuration.VectorCustomEventTypesProvider
 import im.vector.app.features.invite.AutoAcceptInvites
 import im.vector.app.features.invite.CompileTimeAutoAcceptInvites
 import im.vector.app.features.navigation.DefaultNavigator
@@ -141,6 +142,7 @@ import javax.inject.Singleton
             vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider,
             flipperProxy: FlipperProxy,
             vectorPlugins: VectorPlugins,
+            vectorCustomEventTypesProvider: VectorCustomEventTypesProvider,
     ): MatrixConfiguration {
         return MatrixConfiguration(
                 applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
@@ -150,6 +152,7 @@ import javax.inject.Singleton
                         flipperProxy.networkInterceptor(),
                 ),
                 metricPlugins = vectorPlugins.plugins(),
+                customEventTypesProvider = vectorCustomEventTypesProvider,
         )
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt b/vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt
new file mode 100644
index 0000000000..55244685d7
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/configuration/VectorCustomEventTypesProvider.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.configuration
+
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import org.matrix.android.sdk.api.provider.CustomEventTypesProvider
+import javax.inject.Inject
+
+class VectorCustomEventTypesProvider @Inject constructor() : CustomEventTypesProvider {
+
+    override val customPreviewableEventTypes = listOf(
+            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
+    )
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index aaa0fc10c9..c8af85db4f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -21,8 +21,14 @@ import im.vector.app.EmojiSpanify
 import im.vector.app.R
 import im.vector.app.core.extensions.getVectorLastMessageContent
 import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.html.EventHtmlRenderer
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.isLive
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import me.gujun.android.span.image
 import me.gujun.android.span.span
 import org.commonmark.node.Document
 import org.matrix.android.sdk.api.session.events.model.Event
@@ -41,6 +47,7 @@ import javax.inject.Inject
 class DisplayableEventFormatter @Inject constructor(
         private val stringProvider: StringProvider,
         private val colorProvider: ColorProvider,
+        private val drawableProvider: DrawableProvider,
         private val emojiSpanify: EmojiSpanify,
         private val noticeEventFormatter: NoticeEventFormatter,
         private val htmlRenderer: Lazy
@@ -135,6 +142,9 @@ class DisplayableEventFormatter @Inject constructor(
             in EventType.STATE_ROOM_BEACON_INFO.values -> {
                 simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor)
             }
+            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
+                formatVoiceBroadcastEvent(timelineEvent.root.asVoiceBroadcastEvent(), senderName)
+            }
             else -> {
                 span {
                     text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
@@ -252,4 +262,20 @@ class DisplayableEventFormatter @Inject constructor(
             body
         }
     }
+
+    private fun formatVoiceBroadcastEvent(voiceBroadcastEvent: VoiceBroadcastEvent?, senderName: String): CharSequence {
+        return if (voiceBroadcastEvent?.isLive == true) {
+            span {
+                drawableProvider.getDrawable(R.drawable.ic_voice_broadcast, colorProvider.getColor(R.color.palette_vermilion))?.let {
+                    image(it)
+                    +" "
+                }
+                span(stringProvider.getString(R.string.voice_broadcast_live_broadcast)) {
+                    textColor = colorProvider.getColor(R.color.palette_vermilion)
+                }
+            }
+        } else {
+            stringProvider.getString(R.string.notice_voice_broadcast_ended, senderName)
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index 638e3c185d..ca80530261 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
 import im.vector.app.R
 import im.vector.app.core.date.DateFormatKind
 import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.epoxy.VectorEpoxyModel
 import im.vector.app.core.error.ErrorFormatter
 import im.vector.app.core.resources.StringProvider
@@ -29,21 +30,30 @@ import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.RoomListDisplayMode
 import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
 import im.vector.app.features.home.room.typing.TypingHelper
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
 import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.toMatrixItem
 import javax.inject.Inject
 
 class RoomSummaryItemFactory @Inject constructor(
+        private val sessionHolder: ActiveSessionHolder,
         private val displayableEventFormatter: DisplayableEventFormatter,
         private val dateFormatter: VectorDateFormatter,
         private val stringProvider: StringProvider,
         private val typingHelper: TypingHelper,
         private val avatarRenderer: AvatarRenderer,
-        private val errorFormatter: ErrorFormatter
+        private val errorFormatter: ErrorFormatter,
+        private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
 ) {
 
     fun create(
@@ -129,7 +139,7 @@ class RoomSummaryItemFactory @Inject constructor(
         val showSelected = selectedRoomIds.contains(roomSummary.roomId)
         var latestFormattedEvent: CharSequence = ""
         var latestEventTime = ""
-        val latestEvent = roomSummary.latestPreviewableEvent
+        val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
         if (latestEvent != null) {
             latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
             latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@@ -225,4 +235,13 @@ class RoomSummaryItemFactory @Inject constructor(
             else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
         }
     }
+
+    private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
+        val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
+        val liveVoiceBroadcastTimelineEvent = getOngoingVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
+                ?.root?.eventId?.let { room.getTimelineEvent(it) }
+        return liveVoiceBroadcastTimelineEvent
+                ?: latestPreviewableEvent
+                        ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt
index ec50618969..9974db470f 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt
@@ -18,8 +18,8 @@ package im.vector.app.features.voicebroadcast.usecase
 
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.isLive
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.getRoom
@@ -44,6 +44,6 @@ class GetOngoingVoiceBroadcastsUseCase @Inject constructor(
                 QueryStringValue.IsNotEmpty
         )
                 .mapNotNull { it.asVoiceBroadcastEvent() }
-                .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
+                .filter { it.isLive }
     }
 }

From aa5270760e3cbc0b1915a08ab411c733536fb9c7 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Mon, 5 Dec 2022 18:04:54 +0100
Subject: [PATCH 536/679] Hide typing events if there is a live voice broadcast

---
 .../app/features/home/room/list/RoomSummaryItemFactory.kt       | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index ca80530261..d48448b480 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -146,6 +146,8 @@ class RoomSummaryItemFactory @Inject constructor(
         }
 
         val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
+                // Skip typing while there is a live voice broadcast
+                .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
 
         return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
             createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)

From 7a1dfef6d59d49d485883fbdbde794cb140e12f6 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Mon, 5 Dec 2022 18:42:24 +0100
Subject: [PATCH 537/679] Display a notice in the timeline when a voice
 broadcast is stopped

---
 .../src/main/res/values/strings.xml           |  1 +
 .../format/DisplayableEventFormatter.kt       | 10 ++++-----
 .../timeline/format/NoticeEventFormatter.kt   | 21 +++++++++++++++++--
 .../helper/TimelineEventVisibilityHelper.kt   |  2 +-
 4 files changed, 26 insertions(+), 8 deletions(-)

diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 2d289150c6..127d63f74c 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -135,6 +135,7 @@
     The sender\'s device has not sent us the keys for this message.
 
     %1$s ended a voice broadcast.
+    You ended a voice broadcast.
 
     
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index c8af85db4f..5fa9576dd4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -20,13 +20,13 @@ import dagger.Lazy
 import im.vector.app.EmojiSpanify
 import im.vector.app.R
 import im.vector.app.core.extensions.getVectorLastMessageContent
+import im.vector.app.core.extensions.orEmpty
 import im.vector.app.core.resources.ColorProvider
 import im.vector.app.core.resources.DrawableProvider
 import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.html.EventHtmlRenderer
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
 import im.vector.app.features.voicebroadcast.isLive
-import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
 import me.gujun.android.span.image
 import me.gujun.android.span.span
@@ -143,7 +143,7 @@ class DisplayableEventFormatter @Inject constructor(
                 simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor)
             }
             VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
-                formatVoiceBroadcastEvent(timelineEvent.root.asVoiceBroadcastEvent(), senderName)
+                formatVoiceBroadcastEvent(timelineEvent.root, isDm, senderName)
             }
             else -> {
                 span {
@@ -263,8 +263,8 @@ class DisplayableEventFormatter @Inject constructor(
         }
     }
 
-    private fun formatVoiceBroadcastEvent(voiceBroadcastEvent: VoiceBroadcastEvent?, senderName: String): CharSequence {
-        return if (voiceBroadcastEvent?.isLive == true) {
+    private fun formatVoiceBroadcastEvent(event: Event, isDm: Boolean, senderName: String): CharSequence {
+        return if (event.asVoiceBroadcastEvent()?.isLive == true) {
             span {
                 drawableProvider.getDrawable(R.drawable.ic_voice_broadcast, colorProvider.getColor(R.color.palette_vermilion))?.let {
                     image(it)
@@ -275,7 +275,7 @@ class DisplayableEventFormatter @Inject constructor(
                 }
             }
         } else {
-            stringProvider.getString(R.string.notice_voice_broadcast_ended, senderName)
+            noticeEventFormatter.format(event, senderName, isDm).orEmpty()
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index 3f702ed72d..b02e515774 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -22,6 +22,8 @@ import im.vector.app.core.resources.StringProvider
 import im.vector.app.features.roomprofile.permissions.RoleFormatter
 import im.vector.app.features.settings.VectorPreferences
 import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
 import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.extensions.appendNl
 import org.matrix.android.sdk.api.extensions.orFalse
@@ -91,6 +93,9 @@ class NoticeEventFormatter @Inject constructor(
             EventType.CALL_HANGUP,
             EventType.CALL_REJECT,
             EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
+            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
+                formatVoiceBroadcastEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
+            }
             EventType.CALL_NEGOTIATE,
             EventType.CALL_SELECT_ANSWER,
             EventType.CALL_REPLACES,
@@ -109,8 +114,7 @@ class NoticeEventFormatter @Inject constructor(
             EventType.STICKER,
             in EventType.POLL_RESPONSE.values,
             in EventType.POLL_END.values,
-            in EventType.BEACON_LOCATION_DATA.values,
-            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatDebug(timelineEvent.root)
+            in EventType.BEACON_LOCATION_DATA.values -> formatDebug(timelineEvent.root)
             else -> {
                 Timber.v("Type $type not handled by this formatter")
                 null
@@ -191,6 +195,7 @@ class NoticeEventFormatter @Inject constructor(
             EventType.CALL_REJECT,
             EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
             EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
+            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatVoiceBroadcastEvent(event, senderName)
             else -> {
                 Timber.v("Type $type not handled by this formatter")
                 null
@@ -894,4 +899,16 @@ class NoticeEventFormatter @Inject constructor(
                     }
                 }
     }
+
+    private fun formatVoiceBroadcastEvent(event: Event, senderName: String?): CharSequence {
+        return if (event.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED) {
+            if (event.isSentByCurrentUser()) {
+                sp.getString(R.string.notice_voice_broadcast_ended_by_you)
+            } else {
+                sp.getString(R.string.notice_voice_broadcast_ended, senderName)
+            }
+        } else {
+            formatDebug(event)
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
index 382f1c2301..703a5cb911 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt
@@ -252,7 +252,7 @@ class TimelineEventVisibilityHelper @Inject constructor(
         }
 
         if (root.getClearType() == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO &&
-                root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState != VoiceBroadcastState.STARTED) {
+                root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState !in arrayOf(VoiceBroadcastState.STARTED, VoiceBroadcastState.STOPPED)) {
             return true
         }
 

From 35c528405d367eca846b8f775404b2ccf570586e Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 7 Dec 2022 10:15:49 +0100
Subject: [PATCH 538/679] Code cleanup

---
 .../timeline/format/NoticeEventFormatter.kt   | 45 +++++++++----------
 1 file changed, 22 insertions(+), 23 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index b02e515774..a306dd6b2f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -69,33 +69,32 @@ class NoticeEventFormatter @Inject constructor(
     private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
 
     fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? {
-        return when (val type = timelineEvent.root.getClearType()) {
-            EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
-            EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, isDm)
-            EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
-            EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
-            EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            EventType.STATE_ROOM_HISTORY_VISIBILITY ->
-                formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
-            EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
-            EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
+        val event = timelineEvent.root
+        val senderName = timelineEvent.senderInfo.disambiguatedDisplayName
+        return when (val type = event.getClearType()) {
+            EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm)
+            EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(event, isDm)
+            EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
+            EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
+            EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
+            EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, isDm)
+            EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, isDm)
+            EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(event, senderName)
+            EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(event, senderName)
+            EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, isDm)
+            EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(event, senderName)
+            EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(event, senderName, isDm)
+            EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(event, senderName)
             EventType.STATE_ROOM_WIDGET,
-            EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
-            EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
+            EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(event, senderName)
+            EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
+            EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(event, senderName)
             EventType.CALL_INVITE,
             EventType.CALL_CANDIDATES,
             EventType.CALL_HANGUP,
             EventType.CALL_REJECT,
-            EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
-                formatVoiceBroadcastEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
-            }
+            EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
+            VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> formatVoiceBroadcastEvent(event, senderName)
             EventType.CALL_NEGOTIATE,
             EventType.CALL_SELECT_ANSWER,
             EventType.CALL_REPLACES,
@@ -114,7 +113,7 @@ class NoticeEventFormatter @Inject constructor(
             EventType.STICKER,
             in EventType.POLL_RESPONSE.values,
             in EventType.POLL_END.values,
-            in EventType.BEACON_LOCATION_DATA.values -> formatDebug(timelineEvent.root)
+            in EventType.BEACON_LOCATION_DATA.values -> formatDebug(event)
             else -> {
                 Timber.v("Type $type not handled by this formatter")
                 null

From 28c59e3290898b8c3efb29ed9448db94c82842ae Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 6 Dec 2022 10:31:54 +0100
Subject: [PATCH 539/679] Changelog

---
 changelog.d/7719.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7719.feature

diff --git a/changelog.d/7719.feature b/changelog.d/7719.feature
new file mode 100644
index 0000000000..34df6ad964
--- /dev/null
+++ b/changelog.d/7719.feature
@@ -0,0 +1 @@
+Voice Broadcast - Update last message in the room list

From bb7323a93593cf3940fc6323d83c2ee3ed6a8361 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Tue, 6 Dec 2022 17:24:52 +0100
Subject: [PATCH 540/679] Rename some use cases

---
 .../src/main/java/im/vector/app/core/di/VoiceModule.kt |  6 +++---
 .../features/home/room/list/RoomSummaryItemFactory.kt  |  8 +++++---
 .../listening/VoiceBroadcastPlayerImpl.kt              |  4 ++--
 .../usecase/GetLiveVoiceBroadcastChunksUseCase.kt      |  4 ++--
 .../recording/VoiceBroadcastRecorderQ.kt               |  4 ++--
 .../recording/usecase/StartVoiceBroadcastUseCase.kt    |  6 +++---
 .../usecase/StopOngoingVoiceBroadcastUseCase.kt        |  6 +++---
 ...UseCase.kt => GetRoomLiveVoiceBroadcastsUseCase.kt} | 10 ++--------
 ...se.kt => GetVoiceBroadcastStateEventLiveUseCase.kt} |  2 +-
 .../usecase/StartVoiceBroadcastUseCaseTest.kt          |  6 +++---
 10 files changed, 26 insertions(+), 30 deletions(-)
 rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetOngoingVoiceBroadcastsUseCase.kt => GetRoomLiveVoiceBroadcastsUseCase.kt} (84%)
 rename vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/{GetMostRecentVoiceBroadcastStateEventUseCase.kt => GetVoiceBroadcastStateEventLiveUseCase.kt} (99%)

diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
index 6437326294..40fb4ecea7 100644
--- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
@@ -27,7 +27,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
 import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl
 import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
 import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
-import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
 import javax.inject.Singleton
 
 @InstallIn(SingletonComponent::class)
@@ -40,13 +40,13 @@ abstract class VoiceModule {
         fun providesVoiceBroadcastRecorder(
                 context: Context,
                 sessionHolder: ActiveSessionHolder,
-                getMostRecentVoiceBroadcastStateEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
+                getVoiceBroadcastStateEventLiveUseCase: GetVoiceBroadcastStateEventLiveUseCase,
         ): VoiceBroadcastRecorder? {
             return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                 VoiceBroadcastRecorderQ(
                         context = context,
                         sessionHolder = sessionHolder,
-                        getVoiceBroadcastEventUseCase = getMostRecentVoiceBroadcastStateEventUseCase
+                        getVoiceBroadcastEventUseCase = getVoiceBroadcastStateEventLiveUseCase
                 )
             } else {
                 null
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index d48448b480..d8b7a427f0 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -30,8 +30,10 @@ import im.vector.app.features.home.AvatarRenderer
 import im.vector.app.features.home.RoomListDisplayMode
 import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
 import im.vector.app.features.home.room.typing.TypingHelper
+import im.vector.app.features.voicebroadcast.isLive
 import im.vector.app.features.voicebroadcast.isVoiceBroadcast
-import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
 import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.getRoom
@@ -53,7 +55,7 @@ class RoomSummaryItemFactory @Inject constructor(
         private val typingHelper: TypingHelper,
         private val avatarRenderer: AvatarRenderer,
         private val errorFormatter: ErrorFormatter,
-        private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
+        private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
 ) {
 
     fun create(
@@ -240,7 +242,7 @@ class RoomSummaryItemFactory @Inject constructor(
 
     private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
         val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
-        val liveVoiceBroadcastTimelineEvent = getOngoingVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
+        val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
                 ?.root?.eventId?.let { room.getTimelineEvent(it) }
         return liveVoiceBroadcastTimelineEvent
                 ?: latestPreviewableEvent
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index f8025d078e..1bc3078c8b 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -31,7 +31,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat
 import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
 import im.vector.lib.core.utils.timer.CountUpTimer
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.launchIn
@@ -48,7 +48,7 @@ import javax.inject.Singleton
 class VoiceBroadcastPlayerImpl @Inject constructor(
         private val sessionHolder: ActiveSessionHolder,
         private val playbackTracker: AudioMessagePlaybackTracker,
-        private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
+        private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase,
         private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
 ) : VoiceBroadcastPlayer {
 
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
index 03e713eeaa..b2aebd9932 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
@@ -24,7 +24,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.sequence
-import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
 import im.vector.app.features.voicebroadcast.voiceBroadcastId
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -48,7 +48,7 @@ import javax.inject.Inject
  */
 class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
-        private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
+        private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase,
 ) {
 
     fun execute(voiceBroadcast: VoiceBroadcast): Flow> {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
index b751417ca6..2da807293f 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt
@@ -26,7 +26,7 @@ import im.vector.app.features.voice.AbstractVoiceRecorderQ
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
-import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastStateEventLiveUseCase
 import im.vector.lib.core.utils.timer.CountUpTimer
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.launchIn
@@ -40,7 +40,7 @@ import java.util.concurrent.TimeUnit
 class VoiceBroadcastRecorderQ(
         context: Context,
         private val sessionHolder: ActiveSessionHolder,
-        private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase
+        private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventLiveUseCase
 ) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder {
 
     private val session get() = sessionHolder.getActiveSession()
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
index e3814608ea..87ea49cece 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt
@@ -28,7 +28,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
 import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
 import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
-import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
 import im.vector.lib.multipicker.utils.toMultiPickerAudioType
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
@@ -56,7 +56,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
         private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
         private val context: Context,
         private val buildMeta: BuildMeta,
-        private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
+        private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
         private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
 ) {
 
@@ -152,7 +152,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
                 Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast")
                 throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting
             }
-            getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> {
+            getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> {
                 Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting")
                 throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse
             }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt
index 791409b869..fdbf1a067d 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt
@@ -19,7 +19,7 @@ package im.vector.app.features.voicebroadcast.recording.usecase
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
+import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -32,7 +32,7 @@ import javax.inject.Inject
  */
 class StopOngoingVoiceBroadcastUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
-        private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
+        private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
         private val voiceBroadcastHelper: VoiceBroadcastHelper,
 ) {
 
@@ -53,7 +53,7 @@ class StopOngoingVoiceBroadcastUseCase @Inject constructor(
 
         recentRooms
                 .forEach { room ->
-                    val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId)
+                    val ongoingVoiceBroadcasts = getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
                     val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId
                     val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() }
                     if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt
similarity index 84%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt
index 9974db470f..fa5f06bfe6 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt
@@ -23,22 +23,16 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.getRoom
-import timber.log.Timber
 import javax.inject.Inject
 
-class GetOngoingVoiceBroadcastsUseCase @Inject constructor(
+class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
 ) {
 
     fun execute(roomId: String): List {
-        val session = activeSessionHolder.getSafeActiveSession() ?: run {
-            Timber.d("## GetOngoingVoiceBroadcastsUseCase: no active session")
-            return emptyList()
-        }
+        val session = activeSessionHolder.getSafeActiveSession() ?: return emptyList()
         val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
 
-        Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId")
-
         return room.stateService().getStateEvents(
                 setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
                 QueryStringValue.IsNotEmpty
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt
similarity index 99%
rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
rename to vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt
index e0179e403f..b3bbdad635 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt
@@ -42,7 +42,7 @@ import org.matrix.android.sdk.flow.mapOptional
 import timber.log.Timber
 import javax.inject.Inject
 
-class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor(
+class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
         private val session: Session,
 ) {
 
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
index 5b4076378c..5dfdd379e0 100644
--- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt
@@ -52,14 +52,14 @@ class StartVoiceBroadcastUseCaseTest {
     private val fakeRoom = FakeRoom()
     private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
     private val fakeVoiceBroadcastRecorder = mockk(relaxed = true)
-    private val fakeGetOngoingVoiceBroadcastsUseCase = mockk()
+    private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk()
     private val startVoiceBroadcastUseCase = spyk(
             StartVoiceBroadcastUseCase(
                     session = fakeSession,
                     voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
                     context = FakeContext().instance,
                     buildMeta = mockk(),
-                    getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
+                    getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,
                     stopVoiceBroadcastUseCase = mockk()
             )
     )
@@ -140,7 +140,7 @@ class StartVoiceBroadcastUseCaseTest {
         }
                 .mapNotNull { it.asVoiceBroadcastEvent() }
                 .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
-        every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events
+        every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(any()) } returns events
     }
 
     private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)

From 59859ec02ee1eca0a82c41fcb1c1d600f90b0701 Mon Sep 17 00:00:00 2001
From: Florian Renaud 
Date: Wed, 7 Dec 2022 09:56:33 +0100
Subject: [PATCH 541/679] Prioritize call events against live broadcast

---
 .../app/features/home/room/list/RoomSummaryItemFactory.kt     | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index d8b7a427f0..a55900a5c4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -36,6 +36,7 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
 import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
 import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
 import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.room.getTimelineEvent
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
@@ -244,7 +245,8 @@ class RoomSummaryItemFactory @Inject constructor(
         val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
         val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
                 ?.root?.eventId?.let { room.getTimelineEvent(it) }
-        return liveVoiceBroadcastTimelineEvent
+        return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
+                ?: liveVoiceBroadcastTimelineEvent
                 ?: latestPreviewableEvent
                         ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
     }

From 055bf6d3029d8748a9f14ff4de5a24868bb4a2a3 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Wed, 7 Dec 2022 21:41:22 +0300
Subject: [PATCH 542/679] Revert unused companion object.

---
 .../sdk/internal/database/model/RoomAccountDataEntity.kt     | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt
index 2eb5a63784..40040b5738 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt
@@ -24,7 +24,4 @@ import io.realm.annotations.RealmClass
 internal open class RoomAccountDataEntity(
         @Index var type: String? = null,
         var contentStr: String? = null
-) : RealmObject() {
-
-    companion object
-}
+) : RealmObject()

From a5ab1b4a8bdf10bb61a0c2966c63dc6be8837283 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 8 Dec 2022 10:34:08 +0100
Subject: [PATCH 543/679] Fix crash
 `kotlin.UninitializedPropertyAccessException: lateinit property
 avatarRenderer has not been initialized`. AvatarRenderer is not used here.

---
 .../im/vector/app/features/userdirectory/InviteByEmailItem.kt   | 2 --
 1 file changed, 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt
index eaeec35791..da1d76e86a 100644
--- a/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt
+++ b/vector/src/main/java/im/vector/app/features/userdirectory/InviteByEmailItem.kt
@@ -25,12 +25,10 @@ import im.vector.app.R
 import im.vector.app.core.epoxy.ClickListener
 import im.vector.app.core.epoxy.VectorEpoxyHolder
 import im.vector.app.core.epoxy.VectorEpoxyModel
-import im.vector.app.features.home.AvatarRenderer
 
 @EpoxyModelClass
 abstract class InviteByEmailItem : VectorEpoxyModel(R.layout.item_invite_by_mail) {
 
-    @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
     @EpoxyAttribute lateinit var foundItem: ThreePidUser
     @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null
     @EpoxyAttribute var selected: Boolean = false

From 7034d822594a5d7417444c7d613893565e5c6ae9 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 8 Dec 2022 10:36:29 +0100
Subject: [PATCH 544/679] changelog

---
 changelog.d/7744.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7744.bugfix

diff --git a/changelog.d/7744.bugfix b/changelog.d/7744.bugfix
new file mode 100644
index 0000000000..7ed82a9c1c
--- /dev/null
+++ b/changelog.d/7744.bugfix
@@ -0,0 +1 @@
+Fix crash when inviting by email.

From b49045ff15044202a4ac104c39d2ebbf6eae53df Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 8 Dec 2022 10:37:00 +0100
Subject: [PATCH 545/679] Adding changelog entry

---
 changelog.d/7743.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7743.bugfix

diff --git a/changelog.d/7743.bugfix b/changelog.d/7743.bugfix
new file mode 100644
index 0000000000..867c12a3c3
--- /dev/null
+++ b/changelog.d/7743.bugfix
@@ -0,0 +1 @@
+Verification request is not showing when verify session popup is displayed

From b25f185d63b5de9e1cb5523650e7aa7b5623cdc5 Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Thu, 8 Dec 2022 10:48:17 +0100
Subject: [PATCH 546/679] Try to fix issue about danger file not found.

---
 .github/workflows/danger.yml  | 2 +-
 .github/workflows/quality.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index e5226d0723..8752f339bd 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -13,7 +13,7 @@ jobs:
       - name: Danger
         uses: danger/danger-js@11.2.0
         with:
-          args: "--dangerfile tools/danger/dangerfile.js"
+          args: "--dangerfile ./tools/danger/dangerfile.js"
         env:
           DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
           # Fallback for forks
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 57dd5a6a45..fae8d97688 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -68,7 +68,7 @@ jobs:
         if: always()
         uses: danger/danger-js@11.2.0
         with:
-          args: "--dangerfile tools/danger/dangerfile-lint.js"
+          args: "--dangerfile ./tools/danger/dangerfile-lint.js"
         env:
           DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
           # Fallback for forks

From 72ecd1bbc9adcf9fa01f2ed3cfa8e7b86af349a8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 8 Dec 2022 10:51:20 +0100
Subject: [PATCH 547/679] Bump kotlin-gradle-plugin from 1.7.21 to 1.7.22
 (#7664)

Bumps [kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) from 1.7.21 to 1.7.22.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.21...v1.7.22)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index b408ee01eb..a9aee3b681 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -8,7 +8,7 @@ ext.versions = [
 
 def gradle = "7.3.1"
 // Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.7.21"
+def kotlin = "1.7.22"
 def kotlinCoroutines = "1.6.4"
 def dagger = "2.44.2"
 def appDistribution = "16.0.0-beta05"

From d6c20226bb97095f55007d66da6076b8d2b4072e Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Thu, 8 Dec 2022 13:46:01 +0300
Subject: [PATCH 548/679] Add changelog.

---
 changelog.d/7740.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7740.feature

diff --git a/changelog.d/7740.feature b/changelog.d/7740.feature
new file mode 100644
index 0000000000..6cd2b6c776
--- /dev/null
+++ b/changelog.d/7740.feature
@@ -0,0 +1 @@
+Handle account data removal

From de18f37849fa599eb174ea9f5394867b86378670 Mon Sep 17 00:00:00 2001
From: jonnyandrew 
Date: Thu, 8 Dec 2022 11:43:19 +0000
Subject: [PATCH 549/679] [Rich text editor] Add error tracking for rich text
 editor (#7695)

---
 changelog.d/7695.bugfix                       |  1 +
 .../im/vector/app/core/di/SingletonModule.kt  |  4 +++
 .../app/features/analytics/VectorAnalytics.kt |  3 +-
 .../features/analytics/errors/ErrorTracker.kt | 21 +++++++++++++
 .../analytics/impl/DefaultVectorAnalytics.kt  | 14 ++++++---
 .../{SentryFactory.kt => SentryAnalytics.kt}  |  9 ++++--
 .../composer/MessageComposerFragment.kt       |  3 ++
 .../detail/composer/RichTextComposerLayout.kt | 10 ++++--
 .../composer/RichTextEditorException.kt       | 21 +++++++++++++
 .../impl/DefaultVectorAnalyticsTest.kt        | 31 +++++++++++++++----
 ...entryFactory.kt => FakeSentryAnalytics.kt} | 15 +++++++--
 11 files changed, 114 insertions(+), 18 deletions(-)
 create mode 100644 changelog.d/7695.bugfix
 create mode 100644 vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt
 rename vector/src/main/java/im/vector/app/features/analytics/impl/{SentryFactory.kt => SentryAnalytics.kt} (88%)
 create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt
 rename vector/src/test/java/im/vector/app/test/fakes/{FakeSentryFactory.kt => FakeSentryAnalytics.kt} (74%)

diff --git a/changelog.d/7695.bugfix b/changelog.d/7695.bugfix
new file mode 100644
index 0000000000..7ec0805bce
--- /dev/null
+++ b/changelog.d/7695.bugfix
@@ -0,0 +1 @@
+[Rich text editor] Add error tracking for rich text editor
diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt
index 28ca761ace..21a46b0757 100644
--- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt
+++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt
@@ -46,6 +46,7 @@ import im.vector.app.core.utils.AndroidSystemSettingsProvider
 import im.vector.app.core.utils.SystemSettingsProvider
 import im.vector.app.features.analytics.AnalyticsTracker
 import im.vector.app.features.analytics.VectorAnalytics
+import im.vector.app.features.analytics.errors.ErrorTracker
 import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
 import im.vector.app.features.analytics.metrics.VectorPlugins
 import im.vector.app.features.invite.AutoAcceptInvites
@@ -84,6 +85,9 @@ import javax.inject.Singleton
     @Binds
     abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
 
+    @Binds
+    abstract fun bindErrorTracker(analytics: DefaultVectorAnalytics): ErrorTracker
+
     @Binds
     abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker
 
diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt
index 7d11f93883..802ba08092 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt
@@ -16,9 +16,10 @@
 
 package im.vector.app.features.analytics
 
+import im.vector.app.features.analytics.errors.ErrorTracker
 import kotlinx.coroutines.flow.Flow
 
-interface VectorAnalytics : AnalyticsTracker {
+interface VectorAnalytics : AnalyticsTracker, ErrorTracker {
     /**
      * Return a Flow of Boolean, true if the user has given their consent.
      */
diff --git a/vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt
new file mode 100644
index 0000000000..8ad6bfffc0
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/errors/ErrorTracker.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.analytics.errors
+
+interface ErrorTracker {
+    fun trackError(throwable: Throwable)
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
index 553d699d86..ca7608166c 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
@@ -41,7 +41,7 @@ private val IGNORED_OPTIONS: Options? = null
 @Singleton
 class DefaultVectorAnalytics @Inject constructor(
         postHogFactory: PostHogFactory,
-        private val sentryFactory: SentryFactory,
+        private val sentryAnalytics: SentryAnalytics,
         analyticsConfig: AnalyticsConfig,
         private val analyticsStore: AnalyticsStore,
         private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@@ -97,7 +97,7 @@ class DefaultVectorAnalytics @Inject constructor(
         setAnalyticsId("")
 
         // Close Sentry SDK.
-        sentryFactory.stopSentry()
+        sentryAnalytics.stopSentry()
     }
 
     private fun observeAnalyticsId() {
@@ -135,8 +135,8 @@ class DefaultVectorAnalytics @Inject constructor(
     private fun initOrStopSentry() {
         userConsent?.let {
             when (it) {
-                true -> sentryFactory.initSentry()
-                false -> sentryFactory.stopSentry()
+                true -> sentryAnalytics.initSentry()
+                false -> sentryAnalytics.stopSentry()
             }
         }
     }
@@ -180,4 +180,10 @@ class DefaultVectorAnalytics @Inject constructor(
             putAll(this@toPostHogUserProperties.filter { it.value != null })
         }
     }
+
+    override fun trackError(throwable: Throwable) {
+        sentryAnalytics
+                .takeIf { userConsent == true }
+                ?.trackError(throwable)
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryAnalytics.kt
similarity index 88%
rename from vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt
rename to vector/src/main/java/im/vector/app/features/analytics/impl/SentryAnalytics.kt
index a000f2a77a..21721a31ae 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/impl/SentryFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/impl/SentryAnalytics.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.analytics.impl
 
 import android.content.Context
 import im.vector.app.features.analytics.AnalyticsConfig
+import im.vector.app.features.analytics.errors.ErrorTracker
 import im.vector.app.features.analytics.log.analyticsTag
 import io.sentry.Sentry
 import io.sentry.SentryOptions
@@ -25,10 +26,10 @@ import io.sentry.android.core.SentryAndroid
 import timber.log.Timber
 import javax.inject.Inject
 
-class SentryFactory @Inject constructor(
+class SentryAnalytics @Inject constructor(
         private val context: Context,
         private val analyticsConfig: AnalyticsConfig,
-) {
+) : ErrorTracker {
 
     fun initSentry() {
         Timber.tag(analyticsTag.value).d("Initializing Sentry")
@@ -47,4 +48,8 @@ class SentryFactory @Inject constructor(
         Timber.tag(analyticsTag.value).d("Stopping Sentry")
         Sentry.close()
     }
+
+    override fun trackError(throwable: Throwable) {
+        Sentry.captureException(throwable)
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index 97e74785ec..bf9e0ae726 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -60,6 +60,7 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
 import im.vector.app.core.utils.registerForPermissionsResult
 import im.vector.app.databinding.FragmentComposerBinding
 import im.vector.app.features.VectorFeatures
+import im.vector.app.features.analytics.errors.ErrorTracker
 import im.vector.app.features.attachments.AttachmentType
 import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
 import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
@@ -116,6 +117,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
     @Inject lateinit var vectorFeatures: VectorFeatures
     @Inject lateinit var buildMeta: BuildMeta
     @Inject lateinit var session: Session
+    @Inject lateinit var errorTracker: ErrorTracker
 
     private val roomId: String get() = withState(timelineViewModel) { it.roomId }
 
@@ -171,6 +173,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
 
         views.composerLayout.isGone = vectorPreferences.isRichTextEditorEnabled()
         views.richTextComposerLayout.isVisible = vectorPreferences.isRichTextEditorEnabled()
+        views.richTextComposerLayout.setOnErrorListener(errorTracker::trackError)
 
         messageComposerViewModel.observeViewEvents {
             when (it) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index 48459b5c06..16234c3766 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -49,10 +49,11 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
 import im.vector.app.databinding.ViewRichTextMenuButtonBinding
 import io.element.android.wysiwyg.EditorEditText
 import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
+import io.element.android.wysiwyg.utils.RustErrorCollector
 import uniffi.wysiwyg_composer.ActionState
 import uniffi.wysiwyg_composer.ComposerAction
 
-class RichTextComposerLayout @JvmOverloads constructor(
+internal class RichTextComposerLayout @JvmOverloads constructor(
         context: Context,
         attrs: AttributeSet? = null,
         defStyleAttr: Int = 0
@@ -248,10 +249,15 @@ class RichTextComposerLayout @JvmOverloads constructor(
                 updateMenuStateFor(action, state)
             }
         }
-
         updateEditTextVisibility()
     }
 
+    fun setOnErrorListener(onError: (e: RichTextEditorException) -> Unit) {
+        views.richTextComposerEditText.rustErrorCollector = RustErrorCollector {
+            onError(RichTextEditorException(it))
+        }
+    }
+
     private fun updateEditTextVisibility() {
         views.richTextComposerEditText.isVisible = isTextFormattingEnabled
         views.richTextMenu.isVisible = isTextFormattingEnabled
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt
new file mode 100644
index 0000000000..9bdef59ae3
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextEditorException.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.detail.composer
+
+internal class RichTextEditorException(
+        cause: Throwable,
+) : Exception(cause)
diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt
index be53f1b908..3fd0528a19 100644
--- a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt
+++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt
@@ -23,7 +23,7 @@ import im.vector.app.test.fakes.FakeAnalyticsStore
 import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
 import im.vector.app.test.fakes.FakePostHog
 import im.vector.app.test.fakes.FakePostHogFactory
-import im.vector.app.test.fakes.FakeSentryFactory
+import im.vector.app.test.fakes.FakeSentryAnalytics
 import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig
 import im.vector.app.test.fixtures.aUserProperties
 import im.vector.app.test.fixtures.aVectorAnalyticsEvent
@@ -46,11 +46,11 @@ class DefaultVectorAnalyticsTest {
     private val fakePostHog = FakePostHog()
     private val fakeAnalyticsStore = FakeAnalyticsStore()
     private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory()
-    private val fakeSentryFactory = FakeSentryFactory()
+    private val fakeSentryAnalytics = FakeSentryAnalytics()
 
     private val defaultVectorAnalytics = DefaultVectorAnalytics(
             postHogFactory = FakePostHogFactory(fakePostHog.instance).instance,
-            sentryFactory = fakeSentryFactory.instance,
+            sentryAnalytics = fakeSentryAnalytics.instance,
             analyticsStore = fakeAnalyticsStore.instance,
             globalScope = CoroutineScope(Dispatchers.Unconfined),
             analyticsConfig = anAnalyticsConfig(isEnabled = true),
@@ -75,7 +75,7 @@ class DefaultVectorAnalyticsTest {
 
         fakePostHog.verifyOptOutStatus(optedOut = false)
 
-        fakeSentryFactory.verifySentryInit()
+        fakeSentryAnalytics.verifySentryInit()
     }
 
     @Test
@@ -84,7 +84,7 @@ class DefaultVectorAnalyticsTest {
 
         fakePostHog.verifyOptOutStatus(optedOut = true)
 
-        fakeSentryFactory.verifySentryClose()
+        fakeSentryAnalytics.verifySentryClose()
     }
 
     @Test
@@ -111,7 +111,7 @@ class DefaultVectorAnalyticsTest {
 
         fakePostHog.verifyReset()
 
-        fakeSentryFactory.verifySentryClose()
+        fakeSentryAnalytics.verifySentryClose()
     }
 
     @Test
@@ -149,6 +149,25 @@ class DefaultVectorAnalyticsTest {
 
         fakePostHog.verifyNoEventTracking()
     }
+
+    @Test
+    fun `given user has consented, when tracking exception, then submits to sentry`() = runTest {
+        fakeAnalyticsStore.givenUserContent(consent = true)
+        val exception = Exception("test")
+
+        defaultVectorAnalytics.trackError(exception)
+
+        fakeSentryAnalytics.verifySentryTrackError(exception)
+    }
+
+    @Test
+    fun `given user has not consented, when tracking exception, then does not track to sentry`() = runTest {
+        fakeAnalyticsStore.givenUserContent(consent = false)
+
+        defaultVectorAnalytics.trackError(Exception("test"))
+
+        fakeSentryAnalytics.verifyNoErrorTracking()
+    }
 }
 
 private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? {
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSentryAnalytics.kt
similarity index 74%
rename from vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt
rename to vector/src/test/java/im/vector/app/test/fakes/FakeSentryAnalytics.kt
index 2628f80435..59f41543b0 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSentryFactory.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSentryAnalytics.kt
@@ -16,15 +16,15 @@
 
 package im.vector.app.test.fakes
 
-import im.vector.app.features.analytics.impl.SentryFactory
+import im.vector.app.features.analytics.impl.SentryAnalytics
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.verify
 
-class FakeSentryFactory {
+class FakeSentryAnalytics {
     private var isSentryEnabled = false
 
-    val instance = mockk().also {
+    val instance = mockk(relaxUnitFun = true).also {
         every { it.initSentry() } answers  {
             isSentryEnabled = true
         }
@@ -41,4 +41,13 @@ class FakeSentryFactory {
     fun verifySentryClose() {
         verify { instance.stopSentry() }
     }
+
+    fun verifySentryTrackError(error: Throwable) {
+        verify { instance.trackError(error) }
+    }
+
+    fun verifyNoErrorTracking() =
+        verify(inverse = true) {
+            instance.trackError(any())
+        }
 }

From df55c841670ff0d13cf911bd80ce161479c1b33a Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 8 Dec 2022 14:00:35 +0100
Subject: [PATCH 550/679] Raise priority of incoming verification request alert
 + cancel existing verification alerts

---
 .../IncomingVerificationRequestHandler.kt     | 26 ++++++------
 .../vector/app/features/home/HomeActivity.kt  | 40 ++++++++++++-------
 .../app/features/home/HomeDetailFragment.kt   |  2 +-
 .../features/home/NewHomeDetailFragment.kt    |  2 +-
 .../app/features/popup/PopupAlertManager.kt   |  8 +++-
 .../vector/app/features/popup/VectorAlert.kt  |  2 +-
 .../features/popup/VerificationVectorAlert.kt |  1 +
 7 files changed, 50 insertions(+), 31 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
index 3a5c7e7eb8..c749e9578e 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
@@ -72,10 +72,11 @@ class IncomingVerificationRequestHandler @Inject constructor(
                 val user = session.getUserOrDefault(tx.otherUserId).toMatrixItem()
                 val name = user.getBestName()
                 val alert = VerificationVectorAlert(
-                        uid,
-                        context.getString(R.string.sas_incoming_request_notif_title),
-                        context.getString(R.string.sas_incoming_request_notif_content, name),
-                        R.drawable.ic_shield_black,
+                        uid = uid,
+                        title = context.getString(R.string.sas_incoming_request_notif_title),
+                        description = context.getString(R.string.sas_incoming_request_notif_content, name),
+                        iconId = R.drawable.ic_shield_black,
+                        priority = PopupAlertManager.INCOMING_VERIFICATION_REQUEST_PRIORITY,
                         shouldBeDisplayedIn = { activity ->
                             if (activity is VectorBaseActivity<*>) {
                                 // TODO a bit too ugly :/
@@ -85,7 +86,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
                                     }
                                 } ?: true
                             } else true
-                        }
+                        },
                 )
                         .apply {
                             viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get())
@@ -130,8 +131,8 @@ class IncomingVerificationRequestHandler @Inject constructor(
             // if not this request will be underneath and not visible by the user...
             // it will re-appear later
             if (pr.otherUserId == session?.myUserId) {
-                // XXX this is a bit hard coded :/
-                popupAlertManager.cancelAlert("review_login")
+                popupAlertManager.cancelAlert(PopupAlertManager.REVIEW_LOGIN_UID)
+                popupAlertManager.cancelAlert(PopupAlertManager.VERIFY_SESSION_UID)
             }
             val user = session.getUserOrDefault(pr.otherUserId).toMatrixItem()
             val name = user.getBestName()
@@ -142,17 +143,18 @@ class IncomingVerificationRequestHandler @Inject constructor(
             }
 
             val alert = VerificationVectorAlert(
-                    uniqueIdForVerificationRequest(pr),
-                    context.getString(R.string.sas_incoming_request_notif_title),
-                    description,
-                    R.drawable.ic_shield_black,
+                    uid = uniqueIdForVerificationRequest(pr),
+                    title = context.getString(R.string.sas_incoming_request_notif_title),
+                    description = description,
+                    iconId = R.drawable.ic_shield_black,
+                    priority = PopupAlertManager.INCOMING_VERIFICATION_REQUEST_PRIORITY,
                     shouldBeDisplayedIn = { activity ->
                         if (activity is RoomDetailActivity) {
                             activity.intent?.extras?.getParcelableCompat(RoomDetailActivity.EXTRA_ROOM_DETAIL_ARGS)?.let {
                                 it.roomId != pr.roomId
                             } ?: true
                         } else true
-                    }
+                    },
             )
                     .apply {
                         viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get())
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
index 2a3d8d094c..e08ed6db46 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
@@ -437,9 +437,10 @@ class HomeActivity :
     private fun handleAskPasswordToInitCrossSigning(events: HomeActivityViewEvents.AskPasswordToInitCrossSigning) {
         // We need to ask
         promptSecurityEvent(
-                events.userItem,
-                R.string.upgrade_security,
-                R.string.security_prompt_text
+                uid = PopupAlertManager.UPGRADE_SECURITY_UID,
+                userItem = events.userItem,
+                titleRes = R.string.upgrade_security,
+                descRes = R.string.security_prompt_text,
         ) {
             it.navigator.upgradeSessionSecurity(it, true)
         }
@@ -448,9 +449,10 @@ class HomeActivity :
     private fun handleCrossSigningInvalidated(event: HomeActivityViewEvents.OnCrossSignedInvalidated) {
         // We need to ask
         promptSecurityEvent(
-                event.userItem,
-                R.string.crosssigning_verify_this_session,
-                R.string.confirm_your_identity
+                uid = PopupAlertManager.VERIFY_SESSION_UID,
+                userItem = event.userItem,
+                titleRes = R.string.crosssigning_verify_this_session,
+                descRes = R.string.confirm_your_identity,
         ) {
             it.navigator.waitSessionVerification(it)
         }
@@ -459,9 +461,10 @@ class HomeActivity :
     private fun handleOnNewSession(event: HomeActivityViewEvents.CurrentSessionNotVerified) {
         // We need to ask
         promptSecurityEvent(
-                event.userItem,
-                R.string.crosssigning_verify_this_session,
-                R.string.confirm_your_identity
+                uid = PopupAlertManager.VERIFY_SESSION_UID,
+                userItem = event.userItem,
+                titleRes = R.string.crosssigning_verify_this_session,
+                descRes = R.string.confirm_your_identity,
         ) {
             if (event.waitForIncomingRequest) {
                 it.navigator.waitSessionVerification(it)
@@ -474,9 +477,10 @@ class HomeActivity :
     private fun handleCantVerify(event: HomeActivityViewEvents.CurrentSessionCannotBeVerified) {
         // We need to ask
         promptSecurityEvent(
-                event.userItem,
-                R.string.crosssigning_cannot_verify_this_session,
-                R.string.crosssigning_cannot_verify_this_session_desc
+                uid = PopupAlertManager.UPGRADE_SECURITY_UID,
+                userItem = event.userItem,
+                titleRes = R.string.crosssigning_cannot_verify_this_session,
+                descRes = R.string.crosssigning_cannot_verify_this_session_desc,
         ) {
             it.navigator.open4SSetup(it, SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
         }
@@ -485,7 +489,7 @@ class HomeActivity :
     private fun handlePromptToEnablePush() {
         popupAlertManager.postVectorAlert(
                 DefaultVectorAlert(
-                        uid = "enablePush",
+                        uid = PopupAlertManager.ENABLE_PUSH_UID,
                         title = getString(R.string.alert_push_are_disabled_title),
                         description = getString(R.string.alert_push_are_disabled_description),
                         iconId = R.drawable.ic_room_actions_notifications_mutes,
@@ -518,10 +522,16 @@ class HomeActivity :
         )
     }
 
-    private fun promptSecurityEvent(userItem: MatrixItem.UserItem, titleRes: Int, descRes: Int, action: ((VectorBaseActivity<*>) -> Unit)) {
+    private fun promptSecurityEvent(
+            uid: String,
+            userItem: MatrixItem.UserItem,
+            titleRes: Int,
+            descRes: Int,
+            action: ((VectorBaseActivity<*>) -> Unit),
+    ) {
         popupAlertManager.postVectorAlert(
                 VerificationVectorAlert(
-                        uid = "upgradeSecurity",
+                        uid = uid,
                         title = getString(titleRes),
                         description = getString(descRes),
                         iconId = R.drawable.ic_shield_warning
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
index 69abeed424..d310f574dd 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
@@ -156,7 +156,7 @@ class HomeDetailFragment :
         unknownDeviceDetectorSharedViewModel.onEach { state ->
             state.unknownSessions.invoke()?.let { unknownDevices ->
                 if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
-                    val uid = "review_login"
+                    val uid = PopupAlertManager.REVIEW_LOGIN_UID
                     alertManager.cancelAlert(uid)
                     val olderUnverified = unknownDevices.filter { !it.isNew }
                     val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
index ccd5a7e84b..3189c2b99e 100644
--- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
@@ -160,7 +160,7 @@ class NewHomeDetailFragment :
         unknownDeviceDetectorSharedViewModel.onEach { state ->
             state.unknownSessions.invoke()?.let { unknownDevices ->
                 if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
-                    val uid = "review_login"
+                    val uid = PopupAlertManager.REVIEW_LOGIN_UID
                     alertManager.cancelAlert(uid)
                     val olderUnverified = unknownDevices.filter { !it.isNew }
                     val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
index b1327f0caf..e0310b340e 100644
--- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
+++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt
@@ -50,6 +50,12 @@ class PopupAlertManager @Inject constructor(
 
     companion object {
         const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE
+        const val INCOMING_VERIFICATION_REQUEST_PRIORITY = 1
+        const val DEFAULT_PRIORITY = 0
+        const val REVIEW_LOGIN_UID = "review_login"
+        const val UPGRADE_SECURITY_UID = "upgrade_security"
+        const val VERIFY_SESSION_UID = "verify_session"
+        const val ENABLE_PUSH_UID = "enable_push"
     }
 
     private var weakCurrentActivity: WeakReference? = null
@@ -145,7 +151,7 @@ class PopupAlertManager @Inject constructor(
 
     private fun displayNextIfPossible() {
         val currentActivity = weakCurrentActivity?.get()
-        if (Alerter.isShowing || currentActivity == null || currentActivity.isDestroyed) {
+        if (currentActivity == null || currentActivity.isDestroyed) {
             // will retry later
             return
         }
diff --git a/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt
index ffdba8e04d..1597d927d8 100644
--- a/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt
+++ b/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt
@@ -98,7 +98,7 @@ open class DefaultVectorAlert(
 
     override val dismissOnClick: Boolean = true
 
-    override val priority: Int = 0
+    override val priority: Int = PopupAlertManager.DEFAULT_PRIORITY
 
     override val isLight: Boolean = false
 
diff --git a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt
index c2ecbe04b3..818659872f 100644
--- a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt
+++ b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt
@@ -30,6 +30,7 @@ class VerificationVectorAlert(
         title: String,
         override val description: String,
         @DrawableRes override val iconId: Int?,
+        override val priority: Int = PopupAlertManager.DEFAULT_PRIORITY,
         /**
          * Alert are displayed by default, but let this lambda return false to prevent displaying.
          */

From 73fd93148ad012dc0f4ab60fd1b892c1a51e21d0 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Fri, 2 Dec 2022 18:14:58 +0000
Subject: [PATCH 551/679] Download device keys for self prior to verification
 checks

Fixes https://github.com/vector-im/element-android/issues/7676
---
 .../org/matrix/android/sdk/api/rendezvous/Rendezvous.kt     | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
index f724ac4b62..d421f8f994 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
@@ -35,7 +35,10 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.util.MatrixJsonParser
+import org.matrix.android.sdk.api.util.awaitCallback
 import timber.log.Timber
 
 /**
@@ -147,6 +150,9 @@ class Rendezvous(
         val deviceKey = crypto.getMyDevice().fingerprint()
         send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
 
+        // explicitly download keys for ourself rather than wait for initial sync to complete
+        awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) }
+
         // await confirmation of verification
         val verificationResponse = receive()
         if (verificationResponse?.outcome == Outcome.VERIFIED) {

From d0b2c0693de63ff69195c6bb3fac756725e8ac02 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Fri, 2 Dec 2022 18:19:02 +0000
Subject: [PATCH 552/679] Changelog

---
 changelog.d/7699.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7699.bugfix

diff --git a/changelog.d/7699.bugfix b/changelog.d/7699.bugfix
new file mode 100644
index 0000000000..30a4b8e9fa
--- /dev/null
+++ b/changelog.d/7699.bugfix
@@ -0,0 +1 @@
+Fix E2EE set up failure whilst signing in using QR code

From 3a2a916c2f17609fdfba82df08f3160e2685ec68 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Tue, 6 Dec 2022 11:34:25 +0000
Subject: [PATCH 553/679] Clarify comment

---
 .../java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
index d421f8f994..3364e900c6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
@@ -150,7 +150,7 @@ class Rendezvous(
         val deviceKey = crypto.getMyDevice().fingerprint()
         send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
 
-        // explicitly download keys for ourself rather than wait for initial sync to complete
+        // explicitly download keys for ourself rather than racing with initial sync which might not complete in time
         awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) }
 
         // await confirmation of verification

From 7bbd91f2a93adcd9ec73aea618912dda17876236 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Wed, 7 Dec 2022 15:09:43 +0000
Subject: [PATCH 554/679] Handle error whilst download key for self

---
 .../org/matrix/android/sdk/api/rendezvous/Rendezvous.kt  | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
index 3364e900c6..e5b2d6bf12 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
@@ -150,8 +150,13 @@ class Rendezvous(
         val deviceKey = crypto.getMyDevice().fingerprint()
         send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
 
-        // explicitly download keys for ourself rather than racing with initial sync which might not complete in time
-        awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) }
+        try {
+            // explicitly download keys for ourself rather than racing with initial sync which might not complete in time
+            awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) }
+        } catch (e: Throwable) {
+            // log as warning and continue as initial sync might still complete
+            Timber.tag(TAG).w(e, "Failed to download keys for self")
+        }
 
         // await confirmation of verification
         val verificationResponse = receive()

From 63bde230a399bc24e6f33e1ab7254024170c7239 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 8 Dec 2022 14:40:17 +0100
Subject: [PATCH 555/679] Cancel verification alerts when adding the incoming
 request alert and when starting the process

---
 .../IncomingVerificationRequestHandler.kt         | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
index c749e9578e..0f8f5c633e 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
@@ -128,12 +128,9 @@ class IncomingVerificationRequestHandler @Inject constructor(
         // For incoming request we should prompt (if not in activity where this request apply)
         if (pr.isIncoming) {
             // if it's a self verification for my devices, we can discard the review login alert
-            // if not this request will be underneath and not visible by the user...
+            // if not, this request will be underneath and not visible by the user...
             // it will re-appear later
-            if (pr.otherUserId == session?.myUserId) {
-                popupAlertManager.cancelAlert(PopupAlertManager.REVIEW_LOGIN_UID)
-                popupAlertManager.cancelAlert(PopupAlertManager.VERIFY_SESSION_UID)
-            }
+            cancelAnyVerifySessionAlerts(pr)
             val user = session.getUserOrDefault(pr.otherUserId).toMatrixItem()
             val name = user.getBestName()
             val description = if (name == pr.otherUserId) {
@@ -159,6 +156,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
                     .apply {
                         viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get())
                         contentAction = Runnable {
+                            cancelAnyVerifySessionAlerts(pr)
                             (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let {
                                 val roomId = pr.roomId
                                 if (roomId.isNullOrBlank()) {
@@ -188,6 +186,13 @@ class IncomingVerificationRequestHandler @Inject constructor(
         }
     }
 
+    private fun cancelAnyVerifySessionAlerts(pr: PendingVerificationRequest) {
+        if (pr.otherUserId == session?.myUserId) {
+            popupAlertManager.cancelAlert(PopupAlertManager.REVIEW_LOGIN_UID)
+            popupAlertManager.cancelAlert(PopupAlertManager.VERIFY_SESSION_UID)
+        }
+    }
+
     override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
         // If an incoming request is readied (by another device?) we should discard the alert
         if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession || pr.cancelConclusion != null)) {

From b09a00efdaa6666464babb0ce96e1a8cc52ebaee Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Thu, 8 Dec 2022 17:11:09 +0300
Subject: [PATCH 556/679] Code review fixes.

---
 .../sdk/internal/session/room/RoomAPI.kt      |  2 +-
 .../handler/UserAccountDataSyncHandler.kt     | 28 ++++++++++---------
 .../parsing/RoomSyncAccountDataHandler.kt     | 26 +++++++++--------
 .../user/accountdata/AccountDataAPI.kt        |  4 +--
 4 files changed, 32 insertions(+), 28 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 3cf5526a47..4e55b2c40a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -433,7 +433,7 @@ internal interface RoomAPI {
      * @param roomId the room id
      * @param type the type
      */
-    @DELETE(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}")
+    @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}")
     suspend fun deleteRoomAccountData(
             @Path("userId") userId: String,
             @Path("roomId") roomId: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
index fb2dfa10f6..6e8b260da8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
@@ -82,17 +82,13 @@ internal class UserAccountDataSyncHandler @Inject constructor(
 
     fun handle(realm: Realm, accountData: UserAccountDataSync?) {
         accountData?.list?.forEach { event ->
-            if (event.content.isEmpty()) {
-                UserAccountDataEntity.delete(realm, event.type)
-            } else {
-                // Generic handling, just save in base
-                handleGenericAccountData(realm, event.type, event.content)
-                when (event.type) {
-                    UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event)
-                    UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event)
-                    UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event)
-                    UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event)
-                }
+            // Generic handling, just save in base
+            handleGenericAccountData(realm, event.type, event.content)
+            when (event.type) {
+                UserAccountDataTypes.TYPE_DIRECT_MESSAGES -> handleDirectChatRooms(realm, event)
+                UserAccountDataTypes.TYPE_PUSH_RULES -> handlePushRules(realm, event)
+                UserAccountDataTypes.TYPE_IGNORED_USER_LIST -> handleIgnoredUsers(realm, event)
+                UserAccountDataTypes.TYPE_BREADCRUMBS -> handleBreadcrumbs(realm, event)
             }
         }
     }
@@ -261,8 +257,14 @@ internal class UserAccountDataSyncHandler @Inject constructor(
                 .equalTo(UserAccountDataEntityFields.TYPE, type)
                 .findFirst()
         if (existing != null) {
-            // Update current value
-            existing.contentStr = ContentMapper.map(content)
+            if (content.isNullOrEmpty()) {
+                // This is a response for a deleted account data according to
+                // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
+                UserAccountDataEntity.delete(realm, type)
+            } else {
+                // Update current value
+                existing.contentStr = ContentMapper.map(content)
+            }
         } else {
             realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity ->
                 accountDataEntity.type = type
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
index c5f8294077..1128e46298 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
@@ -45,17 +45,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor(
         val roomEntity = RoomEntity.getOrCreate(realm, roomId)
         for (event in accountData.events) {
             val eventType = event.getClearType()
-            if (event.getClearContent().isNullOrEmpty()) {
-                roomEntity.removeAccountData(eventType)
-            } else {
-                handleGeneric(roomEntity, event.getClearContent(), eventType)
-                if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) {
-                    val content = event.getClearContent().toModel()
-                    roomTagHandler.handle(realm, roomId, content)
-                } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) {
-                    val content = event.getClearContent().toModel()
-                    roomFullyReadHandler.handle(realm, roomId, content)
-                }
+            handleGeneric(roomEntity, event.getClearContent(), eventType)
+            if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) {
+                val content = event.getClearContent().toModel()
+                roomTagHandler.handle(realm, roomId, content)
+            } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) {
+                val content = event.getClearContent().toModel()
+                roomFullyReadHandler.handle(realm, roomId, content)
             }
         }
     }
@@ -63,7 +59,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor(
     private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) {
         val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst()
         if (existing != null) {
-            existing.contentStr = ContentMapper.map(content)
+            if (content.isNullOrEmpty()) {
+                // This is a response for a deleted account data according to
+                // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
+                roomEntity.removeAccountData(eventType)
+            } else {
+                existing.contentStr = ContentMapper.map(content)
+            }
         } else {
             val roomAccountData = RoomAccountDataEntity(
                     type = eventType,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
index fd813f1fed..1b3d59ac66 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
@@ -25,7 +25,7 @@ import retrofit2.http.Path
 internal interface AccountDataAPI {
 
     /**
-     * Set some account_data for the client.
+     * Set some account_data for the user.
      *
      * @param userId the user id
      * @param type the type
@@ -39,7 +39,7 @@ internal interface AccountDataAPI {
     )
 
     /**
-     * Remove an account_data for the client.
+     * Remove an account_data for the user.
      *
      * @param userId the user id
      * @param type the type

From 220b1d86c03bc9c68ad77bdbd9d9cba426fe9366 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Thu, 8 Dec 2022 17:41:29 +0100
Subject: [PATCH 557/679] Reverting usage of some stable fields whereas related
 MSCs have not landed into the specs yet

---
 .../room/location/StartLiveLocationShareTask.kt        |  2 +-
 .../session/room/location/StopLiveLocationShareTask.kt |  2 +-
 .../session/room/send/LocalEchoEventFactory.kt         | 10 +++++-----
 .../room/aggregation/poll/PollEventsTestData.kt        |  6 +++---
 .../DefaultGetActiveBeaconInfoForUserTaskTest.kt       |  2 +-
 .../location/DefaultStartLiveLocationShareTaskTest.kt  |  2 +-
 .../location/DefaultStopLiveLocationShareTaskTest.kt   |  2 +-
 .../LiveLocationShareRedactionEventProcessorTest.kt    |  2 +-
 .../vector/app/test/fakes/FakeCreatePollViewStates.kt  |  2 +-
 9 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
index 13753115ac..dd409fe3a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
@@ -46,7 +46,7 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
                 isLive = true,
                 unstableTimestampMillis = clock.epochMillis()
         ).toContent()
-        val eventType = EventType.STATE_ROOM_BEACON_INFO.stable
+        val eventType = EventType.STATE_ROOM_BEACON_INFO.unstable
         val sendStateTaskParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = userId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
index 40f7aa2dd2..e1e6fa9d40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
@@ -45,7 +45,7 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor(
         val sendStateTaskParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = stateKey,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.unstable,
                 body = updatedContent
         )
         return try {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 2f8be69473..8be6b26249 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -181,7 +181,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.stable,
+                type = EventType.POLL_START.unstable,
                 content = newContent.toContent().plus(additionalContent.orEmpty())
         )
     }
@@ -206,7 +206,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_RESPONSE.stable,
+                type = EventType.POLL_RESPONSE.unstable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -226,7 +226,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.stable,
+                type = EventType.POLL_START.unstable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -249,7 +249,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_END.stable,
+                type = EventType.POLL_END.unstable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -300,7 +300,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.BEACON_LOCATION_DATA.stable,
+                type = EventType.BEACON_LOCATION_DATA.unstable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
index bdd1fd9b0d..e38b51132d 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
@@ -87,7 +87,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_START_EVENT = Event(
-            type = EventType.POLL_START.stable,
+            type = EventType.POLL_START.unstable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
@@ -96,7 +96,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_RESPONSE_EVENT = Event(
-            type = EventType.POLL_RESPONSE.stable,
+            type = EventType.POLL_RESPONSE.unstable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
@@ -105,7 +105,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_END_EVENT = Event(
-            type = EventType.POLL_END.stable,
+            type = EventType.POLL_END.unstable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
index 4a10795647..6f416a6bc1 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
@@ -69,7 +69,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest {
         result shouldBeEqualTo currentStateEvent
         fakeStateEventDataSource.verifyGetStateEvent(
                 roomId = params.roomId,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.unstable,
                 stateKey = QueryStringValue.Equals(A_USER_ID)
         )
     }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
index a5c126cf72..3156287774 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
@@ -75,7 +75,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
         val expectedParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = A_USER_ID,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.unstable,
                 body = expectedBeaconContent
         )
         fakeSendStateTask.verifyExecuteRetry(
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
index a7adadfc63..03c6f525e0 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
@@ -79,7 +79,7 @@ class DefaultStopLiveLocationShareTaskTest {
         val expectedSendParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = A_USER_ID,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.unstable,
                 body = expectedBeaconContent
         )
         fakeSendStateTask.verifyExecuteRetry(
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
index d6edb69d93..8dc7a5c9bc 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
@@ -79,7 +79,7 @@ class LiveLocationShareRedactionEventProcessorTest {
     @Test
     fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest {
         val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID)
-        val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.stable)
+        val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.unstable)
         fakeRealm.givenWhere()
                 .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
                 .givenFindFirst(redactedEventEntity)
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt
index 42a500671b..3be1f5c643 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCreatePollViewStates.kt
@@ -63,7 +63,7 @@ object FakeCreatePollViewStates {
     )
 
     private val A_POLL_START_EVENT = Event(
-            type = EventType.POLL_START.stable,
+            type = EventType.POLL_START.unstable,
             eventId = A_FAKE_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_FAKE_USER_ID,

From 99942c271451f1586c599c0232b078aa90026747 Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 9 Dec 2022 09:33:06 +0100
Subject: [PATCH 558/679] Adding changelog entry

---
 changelog.d/7751.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7751.bugfix

diff --git a/changelog.d/7751.bugfix b/changelog.d/7751.bugfix
new file mode 100644
index 0000000000..5d676dbc4d
--- /dev/null
+++ b/changelog.d/7751.bugfix
@@ -0,0 +1 @@
+Revert usage of stable fields in live location sharing and polls

From cf59c80100d7a4b9ec02f62a43a6350e88fe2e0a Mon Sep 17 00:00:00 2001
From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com>
Date: Fri, 9 Dec 2022 09:42:45 +0100
Subject: [PATCH 559/679] stop listening timeline collection changes when app
 is not resumed (#7734)

---
 changelog.d/7643.bugfix                                        | 1 +
 .../vector/app/features/home/room/detail/TimelineFragment.kt   | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 changelog.d/7643.bugfix

diff --git a/changelog.d/7643.bugfix b/changelog.d/7643.bugfix
new file mode 100644
index 0000000000..66e3f28d5f
--- /dev/null
+++ b/changelog.d/7643.bugfix
@@ -0,0 +1 @@
+[Notifications] Fixed a bug when push notification was automatically dismissed while app is on background
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index b73d443832..6ab20275c2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -975,6 +975,7 @@ class TimelineFragment :
         notificationDrawerManager.setCurrentThread(timelineArgs.threadTimelineArgs?.rootThreadEventId)
         roomDetailPendingActionStore.data?.let { handlePendingAction(it) }
         roomDetailPendingActionStore.data = null
+        views.timelineRecyclerView.adapter = timelineEventController.adapter
     }
 
     private fun handlePendingAction(roomDetailPendingAction: RoomDetailPendingAction) {
@@ -993,6 +994,7 @@ class TimelineFragment :
         super.onPause()
         notificationDrawerManager.setCurrentRoom(null)
         notificationDrawerManager.setCurrentThread(null)
+        views.timelineRecyclerView.adapter = null
     }
 
     private val emojiActivityResultLauncher = registerStartForActivityResult { activityResult ->
@@ -1058,7 +1060,6 @@ class TimelineFragment :
             it.dispatchTo(scrollOnHighlightedEventCallback)
         }
         timelineEventController.addModelBuildListener(modelBuildListener)
-        views.timelineRecyclerView.adapter = timelineEventController.adapter
 
         if (vectorPreferences.swipeToReplyIsEnabled()) {
             val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {

From 57cedaeb6924f7df3939ebdd4c7e74d6af0e66cb Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 9 Dec 2022 10:10:59 +0100
Subject: [PATCH 560/679] Adding changelog entry

---
 changelog.d/7753.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7753.bugfix

diff --git a/changelog.d/7753.bugfix b/changelog.d/7753.bugfix
new file mode 100644
index 0000000000..10579b6a84
--- /dev/null
+++ b/changelog.d/7753.bugfix
@@ -0,0 +1 @@
+[Poll] Poll end event is not recognized

From 3d68233723f67d08a6c6bbd819bed5d12d4c5cca Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 9 Dec 2022 14:51:23 +0300
Subject: [PATCH 561/679] Support retrieving account data whose key starts with
 a string.

---
 .../accountdata/SessionAccountDataService.kt        | 13 +++++++++++++
 .../user/accountdata/UserAccountDataDataSource.kt   | 10 ++++++++++
 2 files changed, 23 insertions(+)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt
index a22dd33774..8addb0782e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt
@@ -63,4 +63,17 @@ interface SessionAccountDataService {
      * Update the account data with the provided type and the provided account data content.
      */
     suspend fun updateUserAccountData(type: String, content: Content)
+
+    /**
+     * Retrieve user account data list whose type starts with the given type.
+     * @param type the type or the starting part of a type
+     * @return list of account data whose type starts with the given type
+     */
+    fun getUserAccountDataEventsStartWith(type: String): List
+
+    /**
+     * Deletes user account data of the given type.
+     * @param type the type to delete from user account data
+     */
+    suspend fun deleteUserAccountData(type: String)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
index 39f155096a..2e66f4513b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
@@ -60,6 +60,16 @@ internal class UserAccountDataDataSource @Inject constructor(
         )
     }
 
+    fun getAccountDataEventsStartWith(type: String): List {
+        return realmSessionProvider.withRealm { realm ->
+            realm
+                    .where(UserAccountDataEntity::class.java)
+                    .contains(UserAccountDataEntityFields.TYPE, type)
+                    .findAll()
+                    .map(accountDataMapper::map)
+        }
+    }
+
     private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery {
         val query = realm.where(UserAccountDataEntity::class.java)
         if (types.isNotEmpty()) {

From 8206b534f9d132a02318436549b05b59bd3853d3 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 9 Dec 2022 14:52:27 +0300
Subject: [PATCH 562/679] Create a task to delete an event data with a given
 type.

---
 .../user/accountdata/AccountDataModule.kt     |  3 ++
 .../DefaultSessionAccountDataService.kt       | 11 ++++-
 .../accountdata/DeleteUserAccountDataTask.kt  | 43 +++++++++++++++++++
 3 files changed, 56 insertions(+), 1 deletion(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt
index 3173686a27..463292b9c6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt
@@ -42,4 +42,7 @@ internal abstract class AccountDataModule {
 
     @Binds
     abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
+
+    @Binds
+    abstract fun bindDeleteUserAccountDataTask(task: DefaultDeleteUserAccountDataTask): DeleteUserAccountDataTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
index c73446cf25..304a586a79 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
@@ -34,10 +34,11 @@ import javax.inject.Inject
 internal class DefaultSessionAccountDataService @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         private val updateUserAccountDataTask: UpdateUserAccountDataTask,
+        private val deleteUserAccountDataTask: DeleteUserAccountDataTask,
         private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
         private val userAccountDataDataSource: UserAccountDataDataSource,
         private val roomAccountDataDataSource: RoomAccountDataDataSource,
-        private val taskExecutor: TaskExecutor
+        private val taskExecutor: TaskExecutor,
 ) : SessionAccountDataService {
 
     override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? {
@@ -78,4 +79,12 @@ internal class DefaultSessionAccountDataService @Inject constructor(
             userAccountDataSyncHandler.handleGenericAccountData(realm, type, content)
         }
     }
+
+    override fun getUserAccountDataEventsStartWith(type: String): List {
+        return userAccountDataDataSource.getAccountDataEventsStartWith(type)
+    }
+
+    override suspend fun deleteUserAccountData(type: String) {
+        deleteUserAccountDataTask.execute(DeleteUserAccountDataTask.Params(type))
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt
new file mode 100644
index 0000000000..8d155e32cb
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.user.accountdata
+
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface DeleteUserAccountDataTask : Task {
+
+    data class Params(
+            val type: String,
+    )
+}
+
+internal class DefaultDeleteUserAccountDataTask @Inject constructor(
+        private val accountDataApi: AccountDataAPI,
+        @UserId private val userId: String,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+) : DeleteUserAccountDataTask {
+
+    override suspend fun execute(params: DeleteUserAccountDataTask.Params) {
+        return executeRequest(globalErrorReceiver) {
+            accountDataApi.deleteAccountData(userId, params.type)
+        }
+    }
+}

From 22cce30e353523b4a52085a6201a592fc5a259b2 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 9 Dec 2022 14:53:27 +0300
Subject: [PATCH 563/679] Create use case to detect and delete unnecessary
 account data of client information.

---
 .../DeleteUnusedClientInformationUseCase.kt   | 39 +++++++++++++++++++
 .../settings/devices/v2/DevicesViewModel.kt   | 10 +++++
 2 files changed, 49 insertions(+)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
new file mode 100644
index 0000000000..ae77cf7493
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX
+import javax.inject.Inject
+
+class DeleteUnusedClientInformationUseCase @Inject constructor(
+        private val activeSessionHolder: ActiveSessionHolder,
+) {
+
+    suspend fun execute(deviceFullInfoList: List) {
+        val expectedClientInfoKeyList = deviceFullInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceInfo.deviceId }
+        activeSessionHolder
+                .getSafeActiveSession()
+                ?.accountDataService()
+                ?.getUserAccountDataEventsStartWith(MATRIX_CLIENT_INFO_KEY_PREFIX)
+                ?.map { it.type }
+                ?.subtract(expectedClientInfoKeyList.toSet())
+                ?.forEach { userAccountDataKeyToDelete ->
+                    activeSessionHolder.getSafeActiveSession()?.accountDataService()?.deleteUserAccountData(userAccountDataKeyToDelete)
+                }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
index b7a6c5df30..d8aeefa377 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
@@ -51,6 +51,7 @@ class DevicesViewModel @AssistedInject constructor(
         refreshDevicesUseCase: RefreshDevicesUseCase,
         private val vectorPreferences: VectorPreferences,
         private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase,
+        private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase,
 ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase),
@@ -112,6 +113,9 @@ class DevicesViewModel @AssistedInject constructor(
                         val deviceFullInfoList = async.invoke()
                         val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() }
                         val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive }
+
+                        deleteUnusedClientInformation(deviceFullInfoList)
+
                         copy(
                                 devices = async,
                                 unverifiedSessionsCount = unverifiedSessionsCount,
@@ -125,6 +129,12 @@ class DevicesViewModel @AssistedInject constructor(
                 }
     }
 
+    private fun deleteUnusedClientInformation(deviceFullInfoList: List) {
+        viewModelScope.launch {
+            deleteUnusedClientInformationUseCase.execute(deviceFullInfoList)
+        }
+    }
+
     private fun refreshDevicesOnCryptoDevicesChange() {
         viewModelScope.launch {
             refreshDevicesOnCryptoDevicesChangeUseCase.execute()

From 7a667b513e8f21ca1a58ed58fd6f717dfa52bd67 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 9 Dec 2022 15:47:28 +0300
Subject: [PATCH 564/679] Execute use case from a better place.

---
 .../home/UnknownDeviceDetectorSharedViewModel.kt     | 12 ++++++++++++
 .../v2/DeleteUnusedClientInformationUseCase.kt       |  5 +++--
 .../features/settings/devices/v2/DevicesViewModel.kt |  9 ---------
 3 files changed, 15 insertions(+), 11 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
index 21c7bd6ea1..040ffa7b3f 100644
--- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
@@ -32,11 +32,13 @@ import im.vector.app.core.platform.EmptyViewEvents
 import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.platform.VectorViewModelAction
 import im.vector.app.core.time.Clock
+import im.vector.app.features.settings.devices.v2.DeleteUnusedClientInformationUseCase
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.NoOpMatrixCallback
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.Session
@@ -66,6 +68,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
         private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase,
         private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase,
         private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase,
+        private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase,
 ) : VectorViewModel(initialState) {
 
     sealed class Action : VectorViewModelAction {
@@ -102,6 +105,9 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
         ) { cryptoList, infoList, pInfo ->
             //                    Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}")
 //                    Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
+
+            deleteUnusedClientInformation(infoList)
+
             infoList
                     .filter { info ->
                         // filter verified session, by checking the crypto device info
@@ -143,6 +149,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
         session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
     }
 
+    private fun deleteUnusedClientInformation(deviceFullInfoList: List) {
+        viewModelScope.launch {
+            deleteUnusedClientInformationUseCase.execute(deviceFullInfoList)
+        }
+    }
+
     override fun handle(action: Action) {
         when (action) {
             is Action.IgnoreDevice -> {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
index ae77cf7493..9cbca9664a 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
@@ -18,14 +18,15 @@ package im.vector.app.features.settings.devices.v2
 
 import im.vector.app.core.di.ActiveSessionHolder
 import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 import javax.inject.Inject
 
 class DeleteUnusedClientInformationUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
 ) {
 
-    suspend fun execute(deviceFullInfoList: List) {
-        val expectedClientInfoKeyList = deviceFullInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceInfo.deviceId }
+    suspend fun execute(deviceInfoList: List) {
+        val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId }
         activeSessionHolder
                 .getSafeActiveSession()
                 ?.accountDataService()
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
index d8aeefa377..232fcd50f7 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt
@@ -51,7 +51,6 @@ class DevicesViewModel @AssistedInject constructor(
         refreshDevicesUseCase: RefreshDevicesUseCase,
         private val vectorPreferences: VectorPreferences,
         private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase,
-        private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase,
 ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase),
@@ -114,8 +113,6 @@ class DevicesViewModel @AssistedInject constructor(
                         val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() }
                         val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive }
 
-                        deleteUnusedClientInformation(deviceFullInfoList)
-
                         copy(
                                 devices = async,
                                 unverifiedSessionsCount = unverifiedSessionsCount,
@@ -129,12 +126,6 @@ class DevicesViewModel @AssistedInject constructor(
                 }
     }
 
-    private fun deleteUnusedClientInformation(deviceFullInfoList: List) {
-        viewModelScope.launch {
-            deleteUnusedClientInformationUseCase.execute(deviceFullInfoList)
-        }
-    }
-
     private fun refreshDevicesOnCryptoDevicesChange() {
         viewModelScope.launch {
             refreshDevicesOnCryptoDevicesChangeUseCase.execute()

From bd91db66f8897955682b4f80eed5aa1b2cee06dc Mon Sep 17 00:00:00 2001
From: Maxime NATUREL 
Date: Fri, 9 Dec 2022 14:07:06 +0100
Subject: [PATCH 565/679] Fixing retrieve of related event id in the end poll
 event during aggregation

---
 .../room/aggregation/poll/DefaultPollAggregationProcessor.kt    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
index 90d8e02c39..10c43e3b7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
@@ -156,7 +156,7 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
     override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean {
         val content = event.getClearContent()?.toModel() ?: return false
         val roomId = event.roomId ?: return false
-        val pollEventId = content.relatesTo?.eventId ?: return false
+        val pollEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false
         val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId
         val isPollOwner = pollOwnerId == event.senderId
 

From 85a6c8c6f26bdb4bd61e9ca664e3f0be09125fd5 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Fri, 9 Dec 2022 19:53:20 +0300
Subject: [PATCH 566/679] Write unit tests for the use case.

---
 .../DeleteUnusedClientInformationUseCase.kt   |   3 +
 ...eleteUnusedClientInformationUseCaseTest.kt | 137 ++++++++++++++++++
 .../fakes/FakeSessionAccountDataService.kt    |   4 +
 3 files changed, 144 insertions(+)
 create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt

diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
index 9cbca9664a..d98e839b4f 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
@@ -26,6 +26,9 @@ class DeleteUnusedClientInformationUseCase @Inject constructor(
 ) {
 
     suspend fun execute(deviceInfoList: List) {
+        // A defensive approach against local storage reports an empty device list (although it is not a seen situation).
+        if (deviceInfoList.isEmpty()) return
+
         val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId }
         activeSessionHolder
                 .getSafeActiveSession()
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt
new file mode 100644
index 0000000000..68c9204208
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.settings.devices.v2
+
+import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import io.mockk.coVerify
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+
+private const val A_CURRENT_DEVICE_ID = "current-device-id"
+private const val A_DEVICE_ID_1 = "a-device-id-1"
+private const val A_DEVICE_ID_2 = "a-device-id-2"
+private const val A_DEVICE_ID_3 = "a-device-id-3"
+private const val A_DEVICE_ID_4 = "a-device-id-4"
+
+private val A_DEVICE_INFO_1 = DeviceInfo(deviceId = A_DEVICE_ID_1)
+private val A_DEVICE_INFO_2 = DeviceInfo(deviceId = A_DEVICE_ID_2)
+
+class DeleteUnusedClientInformationUseCaseTest {
+
+    private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+
+    private val deleteUnusedClientInformationUseCase = DeleteUnusedClientInformationUseCase(
+            activeSessionHolder = fakeActiveSessionHolder.instance,
+    )
+
+    @Before
+    fun setup() {
+        fakeActiveSessionHolder.fakeSession.givenSessionId(A_CURRENT_DEVICE_ID)
+    }
+
+    @Test
+    fun `given a device list that account data has all of them and extra devices then use case deletes the unused ones`() = runTest {
+        // Given
+        val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
+        val userAccountDataEventList = listOf(
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3, mapOf("key" to "value")),
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4, mapOf("key" to "value")),
+        )
+        fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
+                type = MATRIX_CLIENT_INFO_KEY_PREFIX,
+                userAccountDataEventList = userAccountDataEventList,
+        )
+
+        // When
+        deleteUnusedClientInformationUseCase.execute(devices)
+
+        // Then
+        coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3) }
+        coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4) }
+    }
+
+    @Test
+    fun `given a device list that account data has exactly all of them then use case does nothing`() = runTest {
+        // Given
+        val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
+        val userAccountDataEventList = listOf(
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
+        )
+        fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
+                type = MATRIX_CLIENT_INFO_KEY_PREFIX,
+                userAccountDataEventList = userAccountDataEventList,
+        )
+
+        // When
+        deleteUnusedClientInformationUseCase.execute(devices)
+
+        // Then
+        coVerify(exactly = 0) {
+            fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
+        }
+    }
+
+    @Test
+    fun `given a device list that account data has missing some of them then use case does nothing`() = runTest {
+        // Given
+        val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
+        val userAccountDataEventList = listOf(
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
+        )
+        fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
+                type = MATRIX_CLIENT_INFO_KEY_PREFIX,
+                userAccountDataEventList = userAccountDataEventList,
+        )
+
+        // When
+        deleteUnusedClientInformationUseCase.execute(devices)
+
+        // Then
+        coVerify(exactly = 0) {
+            fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
+        }
+    }
+
+    @Test
+    fun `given an empty device list that account data has some devices then use case does nothing`() = runTest {
+        // Given
+        val devices = emptyList()
+        val userAccountDataEventList = listOf(
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
+                UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
+        )
+        fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
+                type = MATRIX_CLIENT_INFO_KEY_PREFIX,
+                userAccountDataEventList = userAccountDataEventList,
+        )
+
+        // When
+        deleteUnusedClientInformationUseCase.execute(devices)
+
+        // Then
+        coVerify(exactly = 0) {
+            fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
+        }
+    }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
index f1a0ae7452..cd357ec85a 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt
@@ -54,4 +54,8 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed
     fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) {
         coVerify(inverse = inverse) { updateUserAccountData(type, content) }
     }
+
+    fun givenGetUserAccountDataEventsStartWith(type: String, userAccountDataEventList: List) {
+        every { getUserAccountDataEventsStartWith(type) } returns userAccountDataEventList
+    }
 }

From 74d7e60380caf3a625cd47cf67ef150e1664c821 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 12 Dec 2022 09:21:24 +0100
Subject: [PATCH 567/679] Bump fragment from 1.5.4 to 1.5.5 (#7741)

Bumps `fragment` from 1.5.4 to 1.5.5.

Updates `fragment-ktx` from 1.5.4 to 1.5.5

Updates `fragment-testing` from 1.5.4 to 1.5.5

---
updated-dependencies:
- dependency-name: androidx.fragment:fragment-ktx
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: androidx.fragment:fragment-testing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 dependencies.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies.gradle b/dependencies.gradle
index a9aee3b681..dbb5f5fe05 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -27,7 +27,7 @@ def jjwt = "0.11.5"
 // the whole commit which set version 0.16.0-SNAPSHOT
 def vanniktechEmoji = "0.16.0-SNAPSHOT"
 def sentry = "6.9.0"
-def fragment = "1.5.4"
+def fragment = "1.5.5"
 // Testing
 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
 def espresso = "3.4.0"

From 746fb7719a1224926d7b97eb2e5acdc164771379 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Mon, 12 Dec 2022 13:39:56 +0300
Subject: [PATCH 568/679] Code review fixes.

---
 .../sync/handler/UserAccountDataSyncHandler.kt | 18 ++++++++++--------
 .../sync/parsing/RoomSyncAccountDataHandler.kt | 15 ++++++++-------
 2 files changed, 18 insertions(+), 15 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
index 6e8b260da8..92ebb41ad9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
@@ -253,18 +253,20 @@ internal class UserAccountDataSyncHandler @Inject constructor(
     }
 
     fun handleGenericAccountData(realm: Realm, type: String, content: Content?) {
+        if (content.isNullOrEmpty()) {
+            // This is a response for a deleted account data according to
+            // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
+            UserAccountDataEntity.delete(realm, type)
+            return
+        }
+
         val existing = realm.where()
                 .equalTo(UserAccountDataEntityFields.TYPE, type)
                 .findFirst()
+
         if (existing != null) {
-            if (content.isNullOrEmpty()) {
-                // This is a response for a deleted account data according to
-                // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
-                UserAccountDataEntity.delete(realm, type)
-            } else {
-                // Update current value
-                existing.contentStr = ContentMapper.map(content)
-            }
+            // Update current value
+            existing.contentStr = ContentMapper.map(content)
         } else {
             realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity ->
                 accountDataEntity.type = type
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
index 1128e46298..9da12a2c4a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
@@ -57,15 +57,16 @@ internal class RoomSyncAccountDataHandler @Inject constructor(
     }
 
     private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) {
+        if (content.isNullOrEmpty()) {
+            // This is a response for a deleted account data according to
+            // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
+            roomEntity.removeAccountData(eventType)
+            return
+        }
+
         val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst()
         if (existing != null) {
-            if (content.isNullOrEmpty()) {
-                // This is a response for a deleted account data according to
-                // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
-                roomEntity.removeAccountData(eventType)
-            } else {
-                existing.contentStr = ContentMapper.map(content)
-            }
+            existing.contentStr = ContentMapper.map(content)
         } else {
             val roomAccountData = RoomAccountDataEntity(
                     type = eventType,

From a12167077f378bf0aecbe4e1e4b793457b2902b9 Mon Sep 17 00:00:00 2001
From: Ekaterina Gerasimova 
Date: Fri, 9 Dec 2022 12:19:43 +0000
Subject: [PATCH 569/679] Update project board IDs for automation

"PN-" prefixed IDs are no longer working, update to new IDs
---
 .github/workflows/triage-labelled.yml            | 16 ++++++++--------
 .../workflows/triage-move-review-requests.yml    |  4 ++--
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index 41cd274b93..036bc069ac 100644
--- a/.github/workflows/triage-labelled.yml
+++ b/.github/workflows/triage-labelled.yml
@@ -89,7 +89,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc0sUA"
+          PROJECT_ID: "PVT_kwDOAM0swc0sUA"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
   add_product_issues:
@@ -113,7 +113,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc4AAg6N"
+          PROJECT_ID: "PVT_kwDOAM0swc4AAg6N"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
   delight_issues_to_board:
@@ -139,7 +139,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc1HvQ"
+          PROJECT_ID: "PVT_kwDOAM0swc1HvQ"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
   move_voice-message_issues:
@@ -164,7 +164,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc2KCw"
+          PROJECT_ID: "PVT_kwDOAM0swc2KCw"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
   move_message_bubbles_issues:
     name: A-Message-Bubbles to Message bubbles board
@@ -188,7 +188,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc3m-g"
+          PROJECT_ID: "PVT_kwDOAM0swc3m-g"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
   move_ftue_issues:
@@ -213,7 +213,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc4AAqVx"
+          PROJECT_ID: "PVT_kwDOAM0swc4AAqVx"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
   move_WTF_issues:
@@ -238,7 +238,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc4AArk0"
+          PROJECT_ID: "PVT_kwDOAM0swc4AArk0"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
   move_element_x_issues:
@@ -268,7 +268,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.issue.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc4ABTXY"
+          PROJECT_ID: "PVT_kwDOAM0swc4ABTXY"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
   ps_features1:
diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml
index 6aeba66ccc..f604b82873 100644
--- a/.github/workflows/triage-move-review-requests.yml
+++ b/.github/workflows/triage-move-review-requests.yml
@@ -69,7 +69,7 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.pull_request.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc0sUA"
+          PROJECT_ID: "PVT_kwDOAM0swc0sUA"
           TEAM: "design"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
 
@@ -138,6 +138,6 @@ jobs:
           projectid: ${{ env.PROJECT_ID }}
           contentid: ${{ github.event.pull_request.node_id }}
         env:
-          PROJECT_ID: "PN_kwDOAM0swc4AAg6N"
+          PROJECT_ID: "PVT_kwDOAM0swc4AAg6N"
           TEAM: "product"
           GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

From c523e144b816d313e356c2ab1a1a7db4ff99ba30 Mon Sep 17 00:00:00 2001
From: Jorge Martin Espinosa 
Date: Mon, 12 Dec 2022 13:52:17 +0100
Subject: [PATCH 570/679] Rich text editor: improve performance when changing
 composer mode (#7691)

* Rich text editor: improve performance when changing composer mode

* Add changelog

* Make `MessageComposerMode.Quote` and `Reply` data classes

* Re-arrange code to fix composer not being emptied when sneding a message
---
 changelog.d/7691.bugfix                       |  1 +
 .../composer/MessageComposerFragment.kt       |  2 +-
 .../detail/composer/MessageComposerMode.kt    |  4 +--
 .../detail/composer/RichTextComposerLayout.kt | 25 ++++++++++++++-----
 4 files changed, 23 insertions(+), 9 deletions(-)
 create mode 100644 changelog.d/7691.bugfix

diff --git a/changelog.d/7691.bugfix b/changelog.d/7691.bugfix
new file mode 100644
index 0000000000..0298819143
--- /dev/null
+++ b/changelog.d/7691.bugfix
@@ -0,0 +1 @@
+Rich Text Editor: improve performance when entering reply/edit/quote mode.
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index bf9e0ae726..d56ea8b733 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -285,7 +285,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
                         else -> return
                     }
 
-                    (composer as? RichTextComposerLayout)?.setFullScreen(setFullScreen)
+                    (composer as? RichTextComposerLayout)?.setFullScreen(setFullScreen, true)
 
                     messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(setFullScreen))
                 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt
index a401f04bf5..89cb148639 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt
@@ -23,6 +23,6 @@ sealed interface MessageComposerMode {
 
     sealed class Special(open val event: TimelineEvent, open val defaultContent: CharSequence) : MessageComposerMode
     data class Edit(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
-    class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
-    class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+    data class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+    data class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index 16234c3766..d69fe8edeb 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -66,6 +66,7 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
     // There is no need to persist these values since they're always updated by the parent fragment
     private var isFullScreen = false
     private var hasRelatedMessage = false
+    private var composerMode: MessageComposerMode? = null
 
     var isTextFormattingEnabled = true
         set(value) {
@@ -114,9 +115,15 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
 
     private val dimensionConverter = DimensionConverter(resources)
 
-    fun setFullScreen(isFullScreen: Boolean) {
+    fun setFullScreen(isFullScreen: Boolean, animated: Boolean) {
+        if (!animated && views.composerLayout.layoutParams != null) {
+            views.composerLayout.updateLayoutParams {
+                height =
+                        if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
+            }
+        }
         editText.updateLayoutParams {
-            height = if (isFullScreen) 0 else ViewGroup.LayoutParams.WRAP_CONTENT
+            height = if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
         }
 
         updateTextFieldBorder(isFullScreen)
@@ -371,7 +378,11 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
     override fun renderComposerMode(mode: MessageComposerMode) {
         if (mode is MessageComposerMode.Special) {
             views.composerModeGroup.isVisible = true
-            replaceFormattedContent(mode.defaultContent)
+            if (isTextFormattingEnabled) {
+                replaceFormattedContent(mode.defaultContent)
+            } else {
+                views.plainTextComposerEditText.setText(mode.defaultContent)
+            }
             hasRelatedMessage = true
             editText.showKeyboard(andRequestFocus = true)
         } else {
@@ -383,10 +394,14 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
                     views.plainTextComposerEditText.setText(text)
                 }
             }
-            views.sendButton.contentDescription = resources.getString(R.string.action_send)
             hasRelatedMessage = false
         }
 
+        updateTextFieldBorder(isFullScreen)
+
+        if (this.composerMode == mode) return
+        this.composerMode = mode
+
         views.sendButton.apply {
             if (mode is MessageComposerMode.Edit) {
                 contentDescription = resources.getString(R.string.action_save)
@@ -397,8 +412,6 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
             }
         }
 
-        updateTextFieldBorder(isFullScreen)
-
         when (mode) {
             is MessageComposerMode.Edit -> {
                 views.composerModeTitleView.setText(R.string.editing)

From 8c6c2dd5c2f5ed1777f8da128c82adddf268a589 Mon Sep 17 00:00:00 2001
From: Onuray Sahin 
Date: Mon, 12 Dec 2022 16:36:40 +0300
Subject: [PATCH 571/679] Code review fixes.

---
 changelog.d/7754.feature                      |  1 +
 .../accountdata/UserAccountDataDataSource.kt  |  2 +-
 .../DefaultDeleteUserAccountDataTaskTest.kt   | 53 +++++++++++++++++++
 .../sdk/test/fakes/FakeAccountDataApi.kt      | 32 +++++++++++
 .../DeleteUnusedClientInformationUseCase.kt   |  7 ++-
 .../UnknownDeviceDetectorSharedViewModel.kt   |  2 +-
 ...eleteUnusedClientInformationUseCaseTest.kt |  3 +-
 7 files changed, 92 insertions(+), 8 deletions(-)
 create mode 100644 changelog.d/7754.feature
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt
 rename vector/src/main/java/im/vector/app/{features/settings/devices/v2 => core/session/clientinfo}/DeleteUnusedClientInformationUseCase.kt (87%)
 rename vector/src/test/java/im/vector/app/{features/settings/devices/v2 => core/session/clientinfo}/DeleteUnusedClientInformationUseCaseTest.kt (97%)

diff --git a/changelog.d/7754.feature b/changelog.d/7754.feature
new file mode 100644
index 0000000000..0e1b6d0961
--- /dev/null
+++ b/changelog.d/7754.feature
@@ -0,0 +1 @@
+Delete unused client information from account data
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
index 2e66f4513b..01f5d9f708 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
@@ -64,7 +64,7 @@ internal class UserAccountDataDataSource @Inject constructor(
         return realmSessionProvider.withRealm { realm ->
             realm
                     .where(UserAccountDataEntity::class.java)
-                    .contains(UserAccountDataEntityFields.TYPE, type)
+                    .beginsWith(UserAccountDataEntityFields.TYPE, type)
                     .findAll()
                     .map(accountDataMapper::map)
         }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt
new file mode 100644
index 0000000000..86580127dc
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.user.accountdata
+
+import io.mockk.coVerify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.matrix.android.sdk.test.fakes.FakeAccountDataApi
+import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
+
+private const val A_TYPE = "a-type"
+private const val A_USER_ID = "a-user-id"
+
+@ExperimentalCoroutinesApi
+class DefaultDeleteUserAccountDataTaskTest {
+
+    private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver()
+    private val fakeAccountDataApi = FakeAccountDataApi()
+
+    private val deleteUserAccountDataTask = DefaultDeleteUserAccountDataTask(
+            accountDataApi = fakeAccountDataApi.instance,
+            userId = A_USER_ID,
+            globalErrorReceiver = fakeGlobalErrorReceiver
+    )
+
+    @Test
+    fun `given parameters when executing the task then api is called`() = runTest {
+        // Given
+        val params = DeleteUserAccountDataTask.Params(type = A_TYPE)
+        fakeAccountDataApi.givenParamsToDeleteAccountData(A_USER_ID, A_TYPE)
+
+        // When
+        deleteUserAccountDataTask.execute(params)
+
+        // Then
+        coVerify { fakeAccountDataApi.instance.deleteAccountData(A_USER_ID, A_TYPE) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt
new file mode 100644
index 0000000000..f3acc02458
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI
+
+internal class FakeAccountDataApi {
+
+    val instance: AccountDataAPI = mockk()
+
+    fun givenParamsToDeleteAccountData(userId: String, type: String) {
+        coEvery { instance.deleteAccountData(userId, type) } just runs
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt
similarity index 87%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
rename to vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt
index d98e839b4f..dcd5c58480 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCase.kt
+++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.settings.devices.v2
+package im.vector.app.core.session.clientinfo
 
 import im.vector.app.core.di.ActiveSessionHolder
-import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX
 import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 import javax.inject.Inject
 
@@ -25,9 +24,9 @@ class DeleteUnusedClientInformationUseCase @Inject constructor(
         private val activeSessionHolder: ActiveSessionHolder,
 ) {
 
-    suspend fun execute(deviceInfoList: List) {
+    suspend fun execute(deviceInfoList: List): Result = runCatching {
         // A defensive approach against local storage reports an empty device list (although it is not a seen situation).
-        if (deviceInfoList.isEmpty()) return
+        if (deviceInfoList.isEmpty()) return Result.success(Unit)
 
         val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId }
         activeSessionHolder
diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
index 040ffa7b3f..de9e496ee1 100644
--- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt
@@ -31,8 +31,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
 import im.vector.app.core.platform.EmptyViewEvents
 import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.platform.VectorViewModelAction
+import im.vector.app.core.session.clientinfo.DeleteUnusedClientInformationUseCase
 import im.vector.app.core.time.Clock
-import im.vector.app.features.settings.devices.v2.DeleteUnusedClientInformationUseCase
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt
similarity index 97%
rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt
rename to vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt
index 68c9204208..8acb7b404b 100644
--- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DeleteUnusedClientInformationUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-package im.vector.app.features.settings.devices.v2
+package im.vector.app.core.session.clientinfo
 
-import im.vector.app.core.session.clientinfo.MATRIX_CLIENT_INFO_KEY_PREFIX
 import im.vector.app.test.fakes.FakeActiveSessionHolder
 import io.mockk.coVerify
 import kotlinx.coroutines.test.runTest

From 1930047ce18c1aafa6ea334713d6e733987cac9d Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Wed, 7 Dec 2022 15:15:18 +0000
Subject: [PATCH 572/679] Fix issue of QR not being offered where domain is
 entered instead of homeserver

---
 .../sdk/api/auth/AuthenticationService.kt       |  6 ------
 .../sdk/api/auth/data/LoginFlowResult.kt        |  3 ++-
 .../auth/DefaultAuthenticationService.kt        | 17 ++---------------
 .../features/onboarding/OnboardingViewModel.kt  | 11 ++++++-----
 .../features/onboarding/OnboardingViewState.kt  |  1 +
 .../StartAuthenticationFlowUseCase.kt           |  3 ++-
 6 files changed, 13 insertions(+), 28 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
index 252c33a8c4..e490311b91 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
@@ -125,12 +125,6 @@ interface AuthenticationService {
             deviceId: String? = null
     ): Session
 
-    /**
-     * @param homeServerConnectionConfig the information about the homeserver and other configuration
-     * Return true if qr code login is supported by the server, false otherwise.
-     */
-    suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean
-
     /**
      * Authenticate using m.login.token method during sign in with QR code.
      * @param homeServerConnectionConfig the information about the homeserver and other configuration
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
index 5b6c1897bf..5de83033e1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
@@ -22,5 +22,6 @@ data class LoginFlowResult(
         val isLoginAndRegistrationSupported: Boolean,
         val homeServerUrl: String,
         val isOutdatedHomeserver: Boolean,
-        val isLogoutDevicesSupported: Boolean
+        val isLogoutDevicesSupported: Boolean,
+        val isLoginWithQrSupported: Boolean,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
index 5449c0a735..6556c3a9b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
@@ -299,7 +299,8 @@ internal class DefaultAuthenticationService @Inject constructor(
                 isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
                 homeServerUrl = homeServerUrl,
                 isOutdatedHomeserver = !versions.isSupportedBySdk(),
-                isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
+                isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(),
+                isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(),
         )
     }
 
@@ -408,20 +409,6 @@ internal class DefaultAuthenticationService @Inject constructor(
         )
     }
 
-    override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean {
-        val authAPI = buildAuthAPI(homeServerConnectionConfig)
-        val versions = runCatching {
-            executeRequest(null) {
-                authAPI.versions()
-            }
-        }
-        return if (versions.isSuccess) {
-            versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse()
-        } else {
-            false
-        }
-    }
-
     override suspend fun loginUsingQrLoginToken(
             homeServerConnectionConfig: HomeServerConnectionConfig,
             loginToken: String,
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index 7fe73f8087..b096455611 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor(
         }
     }
 
-    private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) {
+    private fun checkQrCodeLoginCapability() {
         if (!vectorFeatures.isQrCodeLoginEnabled()) {
             setState {
                 copy(
@@ -133,11 +133,9 @@ class OnboardingViewModel @AssistedInject constructor(
                 )
             }
         } else {
-            // check if selected server supports MSC3882 first
-            val canLoginWithQrCode = authenticationService.isQrLoginSupported(config)
             setState {
                 copy(
-                        canLoginWithQrCode = canLoginWithQrCode
+                        canLoginWithQrCode = selectedHomeserver.isLoginWithQrSupported
                 )
             }
         }
@@ -705,7 +703,10 @@ class OnboardingViewModel @AssistedInject constructor(
             // This is invalid
             _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
         } else {
-            startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
+            startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend {
+                checkQrCodeLoginCapability()
+                postAction()
+            })
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
index 6e7d58338e..ea0d940952 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt
@@ -76,6 +76,7 @@ data class SelectedHomeserverState(
         val preferredLoginMode: LoginMode = LoginMode.Unknown,
         val supportedLoginTypes: List = emptyList(),
         val isLogoutDevicesSupported: Boolean = false,
+        val isLoginWithQrSupported: Boolean = false,
 ) : Parcelable
 
 @Parcelize
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt
index db21a53854..9b8f0a1cc4 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt
@@ -47,7 +47,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
             upstreamUrl = authFlow.homeServerUrl,
             preferredLoginMode = preferredLoginMode,
             supportedLoginTypes = authFlow.supportedLoginTypes,
-            isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
+            isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported,
+            isLoginWithQrSupported = authFlow.isLoginWithQrSupported,
     )
 
     private fun LoginFlowResult.findPreferredLoginMode() = when {

From 21cbe527400b627739e87b6efbfcf73826b6bf9d Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Wed, 7 Dec 2022 15:38:25 +0000
Subject: [PATCH 573/679] Lint

---
 .../android/sdk/internal/auth/DefaultAuthenticationService.kt    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
index 6556c3a9b3..d9c2afcb40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
@@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 import org.matrix.android.sdk.api.auth.login.LoginWizard
 import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
 import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
-import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixIdFailure
 import org.matrix.android.sdk.api.session.Session

From 006e2b5c0d1c0a9cc418a6657eab410f3ed8b16d Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Wed, 7 Dec 2022 15:46:47 +0000
Subject: [PATCH 574/679] Changelog

---
 changelog.d/7737.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/7737.bugfix

diff --git a/changelog.d/7737.bugfix b/changelog.d/7737.bugfix
new file mode 100644
index 0000000000..1477834674
--- /dev/null
+++ b/changelog.d/7737.bugfix
@@ -0,0 +1 @@
+Fix issue of Scan QR code button sometimes not showing when it should be available

From 1437f6d41d0ad75e58b5f31b26c6815ba1b0f655 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Fri, 9 Dec 2022 16:59:49 +0000
Subject: [PATCH 575/679] Remove unused bad function call

---
 .../im/vector/app/features/onboarding/OnboardingViewModel.kt    | 2 --
 1 file changed, 2 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index b096455611..04487c6198 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -765,8 +765,6 @@ class OnboardingViewModel @AssistedInject constructor(
             _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
         }
 
-        checkQrCodeLoginCapability(config)
-
         when (trigger) {
             is OnboardingAction.HomeServerChange.SelectHomeServer -> {
                 onHomeServerSelected(config, serverTypeOverride, authResult)

From 643b09a77c9ae9c0ef5b2bdbd4abf16e12414ad1 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Mon, 12 Dec 2022 11:12:44 +0000
Subject: [PATCH 576/679] Fix up unit tests

---
 .../features/onboarding/StartAuthenticationFlowUseCaseTest.kt | 3 ++-
 .../im/vector/app/test/fakes/FakeAuthenticationService.kt     | 4 ----
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt
index d15a6cf042..9be22d7ea9 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt
@@ -140,7 +140,8 @@ class StartAuthenticationFlowUseCaseTest {
             isLoginAndRegistrationSupported = true,
             homeServerUrl = A_DECLARED_HOMESERVER_URL,
             isOutdatedHomeserver = false,
-            isLogoutDevicesSupported = false
+            isLogoutDevicesSupported = false,
+            isLoginWithQrSupported = false
     )
 
     private fun expectedResult(
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
index 5d0e317c57..af53913169 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt
@@ -58,10 +58,6 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
         coEvery { getWellKnownData(matrixId, config) } returns result
     }
 
-    fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) {
-        coEvery { isQrLoginSupported(config) } returns result
-    }
-
     fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) {
         coEvery { getWellKnownData(matrixId, config) } throws cause
     }

From 096e52612e94ce672e7f972f0dceca4eab05be6d Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Mon, 12 Dec 2022 11:18:20 +0000
Subject: [PATCH 577/679] More fix up of unit tests

---
 .../vector/app/features/onboarding/OnboardingViewModelTest.kt   | 2 --
 1 file changed, 2 deletions(-)

diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index 92083eb50b..f41a27ba7d 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -1180,7 +1180,6 @@ class OnboardingViewModelTest {
         fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
         givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
         fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
-        fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode)
     }
 
     private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
@@ -1188,7 +1187,6 @@ class OnboardingViewModelTest {
         fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
         givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
         fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
-        fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false)
     }
 
     private fun givenUserNameIsAvailable(userName: String) {

From f111a84e1745125024c4b7281ec3160874d0aef4 Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Mon, 12 Dec 2022 14:07:01 +0000
Subject: [PATCH 578/679] More unit test fix

---
 .../vector/app/features/onboarding/OnboardingViewModelTest.kt  | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index f41a27ba7d..2649b82f72 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -164,7 +164,7 @@ class OnboardingViewModelTest {
     fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
         val test = viewModel.test()
         fakeVectorFeatures.givenCombinedLoginEnabled()
-        givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true)
+        givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE)
 
         viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
 
@@ -1174,7 +1174,6 @@ class OnboardingViewModelTest {
             resultingState: SelectedHomeserverState,
             config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
             fingerprint: Fingerprint? = null,
-            canLoginWithQrCode: Boolean = false,
     ) {
         fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
         fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))

From 0ffc2af679e9a1980c74cad83489d5a7d08cbcff Mon Sep 17 00:00:00 2001
From: Hugh Nimmo-Smith 
Date: Mon, 12 Dec 2022 17:32:28 +0000
Subject: [PATCH 579/679] Update test to work with new state

---
 .../app/features/onboarding/OnboardingViewModelTest.kt     | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
index 2649b82f72..c570a75d99 100644
--- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
+++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt
@@ -83,6 +83,7 @@ private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
 private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL)
 private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true)
 private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL)
+private val DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED = DEFAULT_SELECTED_HOMESERVER_STATE.copy(isLoginWithQrSupported = true)
 private const val AN_EMAIL = "hello@example.com"
 private const val A_PASSWORD = "a-password"
 private const val A_USERNAME = "hello-world"
@@ -164,7 +165,7 @@ class OnboardingViewModelTest {
     fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
         val test = viewModel.test()
         fakeVectorFeatures.givenCombinedLoginEnabled()
-        givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE)
+        givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED)
 
         viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
 
@@ -173,9 +174,9 @@ class OnboardingViewModelTest {
                         initialState,
                         { copy(onboardingFlow = OnboardingFlow.SignIn) },
                         { copy(isLoading = true) },
-                        { copy(canLoginWithQrCode = true) },
-                        { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) },
+                        { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) },
                         { copy(signMode = SignMode.SignIn) },
+                        { copy(canLoginWithQrCode = true) },
                         { copy(isLoading = false) }
                 )
                 .assertEvents(OnboardingViewEvents.OpenCombinedLogin)

From 4e0c3a97bdd45603ede10b11bd928c79f771bf58 Mon Sep 17 00:00:00 2001
From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com>
Date: Mon, 12 Dec 2022 22:35:09 +0100
Subject: [PATCH 580/679] thread message notification should navigate to thread
 timeline (#7771)

---
 changelog.d/7770.bugfix                       |  1 +
 .../home/room/threads/ThreadsActivity.kt      | 12 ++++-
 .../notifications/NotificationUtils.kt        | 52 +++++++++++++++++--
 .../notifications/RoomGroupMessageCreator.kt  |  9 ++--
 4 files changed, 65 insertions(+), 9 deletions(-)
 create mode 100644 changelog.d/7770.bugfix

diff --git a/changelog.d/7770.bugfix b/changelog.d/7770.bugfix
new file mode 100644
index 0000000000..598deb6073
--- /dev/null
+++ b/changelog.d/7770.bugfix
@@ -0,0 +1 @@
+[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline
diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
index b3f2ef1f75..014b9f0504 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt
@@ -26,6 +26,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack
 import im.vector.app.core.extensions.replaceFragment
 import im.vector.app.core.platform.VectorBaseActivity
 import im.vector.app.databinding.ActivityThreadsBinding
+import im.vector.app.features.MainActivity
 import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
 import im.vector.app.features.analytics.plan.Interaction
 import im.vector.app.features.home.AvatarRenderer
@@ -143,13 +144,20 @@ class ThreadsActivity : VectorBaseActivity() {
                 context: Context,
                 threadTimelineArgs: ThreadTimelineArgs?,
                 threadListArgs: ThreadListArgs?,
-                eventIdToNavigate: String? = null
+                eventIdToNavigate: String? = null,
+                firstStartMainActivity: Boolean = false
         ): Intent {
-            return Intent(context, ThreadsActivity::class.java).apply {
+            val intent = Intent(context, ThreadsActivity::class.java).apply {
                 putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs)
                 putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate)
                 putExtra(THREAD_LIST_ARGS, threadListArgs)
             }
+
+            return if (firstStartMainActivity) {
+                MainActivity.getIntentWithNextIntent(context, intent)
+            } else {
+                intent
+            }
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
index bf1b23093d..7bf78bdb95 100755
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
@@ -60,6 +60,8 @@ import im.vector.app.features.displayname.getBestName
 import im.vector.app.features.home.HomeActivity
 import im.vector.app.features.home.room.detail.RoomDetailActivity
 import im.vector.app.features.home.room.detail.arguments.TimelineArgs
+import im.vector.app.features.home.room.threads.ThreadsActivity
+import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
 import im.vector.app.features.settings.VectorPreferences
 import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver
 import im.vector.app.features.themes.ThemeUtils
@@ -574,6 +576,7 @@ class NotificationUtils @Inject constructor(
     fun buildMessagesListNotification(
             messageStyle: NotificationCompat.MessagingStyle,
             roomInfo: RoomEventGroupInfo,
+            threadId: String?,
             largeIcon: Bitmap?,
             lastMessageTimestamp: Long,
             senderDisplayNameForReplyCompat: String?,
@@ -581,7 +584,11 @@ class NotificationUtils @Inject constructor(
     ): Notification {
         val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
         // Build the pending intent for when the notification is clicked
-        val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId)
+        val openIntent = when {
+            threadId != null && vectorPreferences.areThreadMessagesEnabled() -> buildOpenThreadIntent(roomInfo, threadId)
+            else -> buildOpenRoomIntent(roomInfo.roomId)
+        }
+
         val smallIcon = R.drawable.ic_notification
 
         val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
@@ -666,8 +673,8 @@ class NotificationUtils @Inject constructor(
                         }
                     }
 
-                    if (openRoomIntent != null) {
-                        setContentIntent(openRoomIntent)
+                    if (openIntent != null) {
+                        setContentIntent(openIntent)
                     }
 
                     if (largeIcon != null) {
@@ -826,6 +833,45 @@ class NotificationUtils @Inject constructor(
                 )
     }
 
+    private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? {
+        val threadTimelineArgs = ThreadTimelineArgs(
+                startsThread = false,
+                roomId = roomInfo.roomId,
+                rootThreadEventId = threadId,
+                showKeyboard = false,
+                displayName = roomInfo.roomDisplayName,
+                avatarUrl = null,
+                roomEncryptionTrustLevel = null,
+        )
+        val threadIntentTap = ThreadsActivity.newIntent(
+                context = context,
+                threadTimelineArgs = threadTimelineArgs,
+                threadListArgs = null,
+                firstStartMainActivity = true,
+        )
+        threadIntentTap.action = actionIds.tapToView
+        // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
+        threadIntentTap.data = createIgnoredUri("openThread?$threadId")
+
+        val roomIntent = RoomDetailActivity.newIntent(
+                context = context,
+                timelineArgs = TimelineArgs(
+                        roomId = roomInfo.roomId,
+                        switchToParentSpace = true
+                ),
+                firstStartMainActivity = false
+        )
+        // Recreate the back stack
+        return TaskStackBuilder.create(context)
+                .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false))
+                .addNextIntentWithParentStack(roomIntent)
+                .addNextIntent(threadIntentTap)
+                .getPendingIntent(
+                        clock.epochMillis().toInt(),
+                        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
+                )
+    }
+
     private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
         val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true)
         intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt
index 7fdfa3535e..767f427f39 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt
@@ -33,14 +33,14 @@ class RoomGroupMessageCreator @Inject constructor(
 ) {
 
     fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
-        val firstKnownRoomEvent = events[0]
-        val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: ""
-        val roomIsGroup = !firstKnownRoomEvent.roomIsDirect
+        val lastKnownRoomEvent = events.last()
+        val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: ""
+        val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
         val style = NotificationCompat.MessagingStyle(
                 Person.Builder()
                         .setName(userDisplayName)
                         .setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
-                        .setKey(firstKnownRoomEvent.matrixID)
+                        .setKey(lastKnownRoomEvent.matrixID)
                         .build()
         ).also {
             it.conversationTitle = roomName.takeIf { roomIsGroup }
@@ -75,6 +75,7 @@ class RoomGroupMessageCreator @Inject constructor(
                             it.customSound = events.last().soundName
                             it.isUpdated = events.last().isUpdated
                         },
+                        threadId = lastKnownRoomEvent.threadId,
                         largeIcon = largeBitmap,
                         lastMessageTimestamp,
                         userDisplayName,

From d05e10e10a952bfc1cdee8cdeab49585caa46ca0 Mon Sep 17 00:00:00 2001
From: Valere 
Date: Tue, 13 Dec 2022 11:38:49 +0100
Subject: [PATCH 581/679] crypto migration tests (#7645)

Crypto migration tests

Co-authored-by: Benoit Marty 
---
 .gitattributes                                |   1 +
 changelog.d/7645.misc                         |   1 +
 docs/database_migration_test.md               |  55 +++++++++++++++
 .../androidTest/assets/crypto_store_20.realm  |   3 +
 .../src/androidTest/assets/session_42.realm   | Bin 270336 -> 131 bytes
 .../database/CryptoSanityMigrationTest.kt     |  65 ++++++++++++++++++
 6 files changed, 125 insertions(+)
 create mode 100644 changelog.d/7645.misc
 create mode 100644 docs/database_migration_test.md
 create mode 100644 matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm
 create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt

diff --git a/.gitattributes b/.gitattributes
index 0542767eff..b44f3fab1b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
 **/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
+**/src/androidTest/assets/*.realm filter=lfs diff=lfs merge=lfs -text
diff --git a/changelog.d/7645.misc b/changelog.d/7645.misc
new file mode 100644
index 0000000000..a133581ac1
--- /dev/null
+++ b/changelog.d/7645.misc
@@ -0,0 +1 @@
+Crypto database migration tests
diff --git a/docs/database_migration_test.md b/docs/database_migration_test.md
new file mode 100644
index 0000000000..f7844abde8
--- /dev/null
+++ b/docs/database_migration_test.md
@@ -0,0 +1,55 @@
+
+
+* [Testing database migration](#testing-database-migration)
+  * [Creating a reference database](#creating-a-reference-database)
+  * [Testing](#testing)
+
+
+
+## Testing database migration
+
+### Creating a reference database
+
+Databases are encrypted, the key to decrypt is needed to setup the test.
+A special build property must be enabled to extract it. 
+
+Set `vector.debugPrivateData=true` in `~/.gradle/gradle.properties` (to avoid committing by mistake)
+
+Launch the app in your emulator, login and use the app to fill up the database.
+
+Save the key for the tested database
+```
+RealmKeysUtils  W  Database key for alias `session_db_fe9f212a611ccf6dea1141777065ed0a`: 935a6dfa0b0fc5cce1414194ed190....
+RealmKeysUtils  W  Database key for alias `crypto_module_fe9f212a611ccf6dea1141777065ed0a`: 7b9a21a8a311e85d75b069a343.....
+```
+
+
+Use the [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer) to extrat the database file from the emulator.
+
+Go to `data/data/im.vector.app.debug/files//`
+Pick the database you want to test (name can be found in SessionRealmConfigurationFactory):
+ - crypto_store.realm for crypto
+ - disk_store.realm for session
+ - etc... 
+
+Download the file on your disk
+
+### Testing
+
+Copy the file in `src/AndroidTest/assets`
+
+see `CryptoSanityMigrationTest` or `RealmSessionStoreMigration43Test` for sample tests.
+
+There are already some databases in the assets folder.
+The existing test will properly detect schema changes, and fail with such errors if a migration is missing:
+
+```
+io.realm.exceptions.RealmMigrationNeededException: Migration is required due to the following errors:
+- Property 'CryptoMetadataEntity.foo' has been added.
+```
+
+If you want to test properly more complex database migration (dynamic transforms) ensure that the database contains
+the entity you want to migrate.
+
+You can explore the database with [realm studio](https://www.mongodb.com/docs/realm/studio/) if needed.
+
diff --git a/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm b/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm
new file mode 100644
index 0000000000..cfdd2e6da6
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/assets/crypto_store_20.realm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a7acd69f37612bab0a1ab7f456656712d7ba19dbb679f81b97b58ef44e239f42
+size 8523776
diff --git a/matrix-sdk-android/src/androidTest/assets/session_42.realm b/matrix-sdk-android/src/androidTest/assets/session_42.realm
index b92d13dab2fa6d44a38ee3443ae8cad85340db23..92681116994c0fc88108bb9b54d22b86f5b260bb 100644
GIT binary patch
literal 131
zcmWN^%MHUI3;@tOQ?Nk8#{rXU0|p9GTcVoe(CM4g)4R%-`}oK<=fS&D&psco%FAs#
z(?a8|_$Z_c$f)Xm=V}uzYe*<(|cmoKSLqc$0fH_W4pTxnK)j1
zA+S-db^-$c0AQ!RSAv=t6ebpw5F8Icz0)Qare3NEO!V;J+@T>DqBCS#BCrYTgKu7$=_P
z9|I4c#|Hjbt6z=iY$84^f8&fxjGXeMBXEBmdWX}E1_3ry-E67w&1%()ryy(Y$$fO#
zZHI^l=huHC@BHJaHq3?OE#`;A`^uEGwN6YrPP;wihl3b@ea9`vjDFi1-~h`l;=Pbt
zw)rUGxDOP@!FC))iQ(62Y9N=Ac8dPs|EpP0roM8Rd;oaKmFwZJ;1qQx{&ka`@lG!>
zH-N#*xe(sfBb`<<|vVymn6O4)cw4sVuQmG4ZlW5hGo7#MXB5_7?fnSI3T1!w}o*L+7
zP$J?-luHT+s{BlI2)$U|(p{4Jb*s=~sl0L57%Ux=4f(YZ>LhIx>06ePq@9d>2+3dn
zq!NptN(1|sQof1j_t|kJU1eb9zB44CV6-+U^6TYEoaJj#Kw~k~o60d2J%et;FUw*r
zVK`d*L_ouI5pi`zfR*pDiQdQysMGORrf9$t9?HUBvPk42Z4zz$}REhXIa9|
zhe}jVIt58Oo}jk_We@g0Xygs;l|Ai9;=c~PQ15mKln{oss&cjsqhK5oDt{bIxm*;qE3sIV@anMu=}7K4Zk2ysK!-^d?I??_`l6h*?^Ql;
z+ds_3+-siw6BBHg^4I+Y7cmTq@Lb^Bqk+t#uN8pE)gksqwM%+~D%U7}eV=Yrp}dCI
z_Uif8OgnZ^N(9&^r>?DSh4$hogYMT?kl5QOQMFnLxQ7jF!dl5UiM7su0>9i_oT(UT
ze_hA=B;le%1x&re(2Dd8g^XWHiuJy7_RUU{D~tcvTNNc8Ud#asMv+XA5fH82r6Du+
z?GcRQM!n(y`TpyFcBlJ%mz2R@Si8WB8GQ&&`K)S^BUL}4^dInl?ZXpsW3FIbWOmBi
zD#H^{|B0Vn+`QG!Qx7`T8K(Jlu(niL{;4bGY(j8O_cRRpbymVY@?
zR%6iup9_mI#wzI$c}D#yDEWaxrAL(LzfKG!WixWF9v@0DllGV0(++Ff4EUsu3!sL2
z%nt3>rIImuy2Lt!)#&`ZRBZ`dWtWzSX!RlT9Iaws|C|4;ISiqZyZwX^&QY*315)>8
z5N}T4d2MWWTqKtNoBt^B3Boh8%e_RACk0GP;n4r*-!S(#!
z|Mmx~g+yG`g-VEDmTBT3WKlt@bStDbOObR-BmcFJ%+AR^c_`bkhm**p9w`V;ZTR=d
zWs=xM%SgF{9NDVB=Nh?JH5{r}o;`VrXu;0THgWpF4Y>YpPzN
zk4y6?6gxR2up3IQ_S{k!`Qe(bZrdonXwngf8$FGKv-wd
zTPPL8xec>9f&AijNExmi|HJ-Bzxegv_)^^b=~_#)*C3F=wPo!43RK6sZ$__c@*yA&
zpHP0ite?c^<<{KfC`
z4It6wbKKDJen{;GiGcDMgBL3)sp6N!S=XIqX#U^&dQEs%;W5=AR21X+ddBgBfY6dU
zlId-wI&L|ODgtyN*?cl%q*#4Do(%f4O%R7@~F=?T6Qqlqh^h6TWPCqY){#TzA
zXg-$TDYory9MM3X`R7D@w~!2gt`Qn{eFq}oufHxwp-Ex(NZ`wDTXm#yAwB%13#IQls9_aVATBGLREeXJ;asv+5V}T&Up<@Gxhu_0@%Fx
ztI9@bh)Ym1B4|>mK3voI2S^!-l1bguVF+DIIXFQy8I0hWW#RVFJFn~ClwpZtFCBg-
zm7j}sk{fgM?G3$#jzcj>HbRa17S91hno6By;#KbIUD|-Ege#-1ij2&{izp*!Y5rI#
znW{h~_j;0kiy#7ea&CAt;`{`F8m@
z|K3M@!f$YlIyj<)XSgS{iuf8{j5;{&;@0~}H7n4fncezrSaadst@0+kNuIn*1cRplgx<4g;?LewD&b$b9#61dM
z$*_^2-Hc{X*PJN26__+t8nxlc0E5W&kZHL=OHD)U*6A|Xk9K#_nmCU-A?);u@YmWB
zvVZMGZGY3h>0|p4e=pmkYpP#MB3VO%DNn)j6sRt{{wSV1NL3_Iibc?TEY
z7_RdQ`;PmZHP9+zNczFtZpxmD4=Y(Szc4O=30pST^-3Xg8gKR#*vHn5gHR}+DuR(s
z>bcV2){w-h2(qq50rDjh6%z}A=3gxSgOLNgP0-ZAaMh5tCHFoWnv?iWFuQ@+cX)Bw
z4>75tN(lI&ha^zC_wywi2yt{
zj5@TfJ15{%I>Etl>Gfi#Z?)%>F)Afd;pe5RW#n)Ms-Htx_bHk7_OU
zGmdbzA2p7(p^@P(xmqB3b6PXT6Y*#sGbDtXF(?3h3O@eNFlj~
znjl%f5+?BA`#Hnq8g;Zv=LV=&cxa@$a?<_iBqYtl^WPMz?Ex(&E&4_jID1TVQ81N#
zjGu(+P5k@S8v$SBGxL==2J^sAR%x$gv$!EsmUFba*uUfW#vzVc4{x}(Zz1s`&S$I0
zYz@y48QdiugTK=YIY51{x}OD4TZi*T9;3W{QG1^Bdibq+1!tiCCF{wo8+
zMet(L?Q!rOh(K-Ol4BzqM?`aEQTGexr1NwgHIFwq242c&LqR|MH))<7mnPzkKOH->
z6dGjT!4>RAF_qyOZk4v`hBJ?UtzbQGzPn#CAr0t$BGfSL036}@Hw~kDuB1;z?1aq;!izkYB!o%m}&Tj<9qWNoHaPPS|bW{vY~Rc5Yv{m^fDMQp=ed!sINW3m4aAEkkky7+Rv1
zJgBRmxUKVla9Z|cL1^s?3=1PyvW*ClN)nPO(CLDgY?{%MO%Y)0)M)x$P$2H&ghTc+
zH>eazvQ+ylef%+OTL{=i0=$+aXuOSGC#=^e;lV
zF129P(3l@y{Ju9M@nW*?W*n4)7(kwXe+7ay$qFlmDN|3`S9dp7n5xi1)+j5%SozKP
z%oGbpUIRa@pBs}hTM>?m&cXO=8gzkEFd4bFo!g;9ps}

~n*FN$6ddX2RG}!itYe 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%0DTX_=9PRTAUr6@kN4E#1dFf1 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 L00KbZ|0M7~<4-Vn diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt new file mode 100644 index 0000000000..2643bf643a --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.util.time.Clock + +class CryptoSanityMigrationTest { + @get:Rule val configurationFactory = TestRealmConfigurationFactory() + + lateinit var context: Context + var realm: Realm? = null + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + realm?.close() + } + + @Test + fun cryptoDatabaseShouldMigrateGracefully() { + val realmName = "crypto_store_20.realm" + val migration = RealmCryptoStoreMigration(object : Clock { + override fun epochMillis(): Long { + return 0L + } + }) + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca", + RealmCryptoStoreModule(), + migration.schemaVersion, + migration + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + realm = Realm.getInstance(realmConfiguration) + } +} From 4657729e3619c61d7767e0bbd78f6a12e2926410 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 12:41:17 +0000 Subject: [PATCH 582/679] Bump dependency-check-gradle from 7.3.0 to 7.4.1 (#7759) Bumps dependency-check-gradle from 7.3.0 to 7.4.1. --- updated-dependencies: - dependency-name: org.owasp:dependency-check-gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2abb2a9072..0f94fc418c 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.2.3" - classpath 'org.owasp:dependency-check-gradle:7.3.0' + classpath 'org.owasp:dependency-check-gradle:7.4.1' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' From 3dadebe505d52a86ab1b1c71d3969ac35a77cf2e Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Tue, 13 Dec 2022 14:02:45 +0100 Subject: [PATCH 583/679] threads are enabled by default end forced to enabled for existing users (#7775) --- changelog.d/5503.misc | 1 + .../android/sdk/api/MatrixConfiguration.kt | 2 +- .../src/main/res/values/config-settings.xml | 2 +- .../features/home/HomeActivityViewModel.kt | 6 ++++++ .../features/settings/VectorPreferences.kt | 19 +++++++++++++++++++ .../labs/VectorSettingsLabsFragment.kt | 1 + 6 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5503.misc diff --git a/changelog.d/5503.misc b/changelog.d/5503.misc new file mode 100644 index 0000000000..66deb33684 --- /dev/null +++ b/changelog.d/5503.misc @@ -0,0 +1 @@ +[Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 00d74ab446..8c2296accb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -66,7 +66,7 @@ data class MatrixConfiguration( /** * Thread messages default enable/disabled value. */ - val threadMessagesEnabledDefault: Boolean = false, + val threadMessagesEnabledDefault: Boolean = true, /** * List of network interceptors, they will be added when building an OkHttp client. */ diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index ad9c16c214..a8695eed44 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -39,7 +39,7 @@ true true - false + true true false true diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index a54ce2cff3..8f16121a30 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -254,6 +254,12 @@ class HomeActivityViewModel @AssistedInject constructor( // } when { + !vectorPreferences.areThreadMessagesEnabled() && !vectorPreferences.wasThreadFlagChangedManually() -> { + vectorPreferences.setThreadMessagesEnabled() + lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) + // Clear Cache + _viewEvents.post(HomeActivityViewEvents.MigrateThreads(checkSession = false)) + } // Notify users vectorPreferences.shouldNotifyUserAboutThreads() && vectorPreferences.areThreadMessagesEnabled() -> { Timber.i("----> Notify users about threads") diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index d46b819cce..2d5fb351f9 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -239,6 +239,7 @@ class VectorPreferences @Inject constructor( // This key will be used to identify clients with the new thread support enabled m.thread const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL" + const val SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER = "SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER" const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED" // This key will be used to enable user for displaying live user info or not. @@ -1129,6 +1130,24 @@ class VectorPreferences @Inject constructor( .apply() } + /** + * Indicates whether or not user changed threads flag manually. We need this to not force flag to be enabled on app start. + * Should be removed when Threads flag will be removed + */ + fun wasThreadFlagChangedManually(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER, false) + } + + /** + * Sets the flag to indicate that user changed threads flag (e.g. disabled them). + */ + fun setThreadFlagChangedManually() { + defaultPrefs + .edit() + .putBoolean(SETTINGS_LABS_THREAD_MESSAGES_CHANGED_BY_USER, true) + .apply() + } + /** * Indicates whether or not the user will be notified about the new thread support. * We should notify the user only if he had old thread support enabled. diff --git a/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt index c10411301f..189d55d990 100644 --- a/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/labs/VectorSettingsLabsFragment.kt @@ -141,6 +141,7 @@ class VectorSettingsLabsFragment : */ private fun onThreadsPreferenceClicked() { // We should migrate threads only if threads are disabled + vectorPreferences.setThreadFlagChangedManually() vectorPreferences.setShouldMigrateThreads(!vectorPreferences.areThreadMessagesEnabled()) lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled()) displayLoadingView() From 71df1e61d43782f7a181f267ab75e1f173dbbc0a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 13 Dec 2022 15:45:46 +0100 Subject: [PATCH 584/679] Remove non necessary call when getting the targeted event id --- .../room/aggregation/poll/DefaultPollAggregationProcessor.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 10c43e3b7f..58583f8a91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -78,7 +78,7 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro val content = event.getClearContent()?.toModel() ?: return false val roomId = event.roomId ?: return false val senderId = event.senderId ?: return false - val targetEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false + val targetEventId = event.getRelationContent()?.eventId ?: return false val targetPollContent = getPollContent(session, roomId, targetEventId) ?: return false val annotationsSummaryEntity = getAnnotationsSummaryEntity(realm, roomId, targetEventId) @@ -154,9 +154,8 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro } override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean { - val content = event.getClearContent()?.toModel() ?: return false val roomId = event.roomId ?: return false - val pollEventId = (event.getRelationContent() ?: content.relatesTo)?.eventId ?: return false + val pollEventId = event.getRelationContent()?.eventId ?: return false val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId val isPollOwner = pollOwnerId == event.senderId From 96e29d4d10e4fd3a9749c714dedd31d17bb44054 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 13 Dec 2022 15:46:14 +0100 Subject: [PATCH 585/679] Renaming the name of the test file be consistent --- ...nProcessorTest.kt => DefaultPollAggregationProcessorTest.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/{PollAggregationProcessorTest.kt => DefaultPollAggregationProcessorTest.kt} (99%) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt similarity index 99% rename from matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index 3044ca5d43..c1fd615e25 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -47,7 +47,7 @@ import org.matrix.android.sdk.test.fakes.FakeRealm import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenFindFirst -class PollAggregationProcessorTest { +class DefaultPollAggregationProcessorTest { private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() private val realm = FakeRealm() From 851276978f48811de842e36f75bf16d430dcce07 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 13 Dec 2022 15:47:30 +0100 Subject: [PATCH 586/679] Remove unused import --- .../room/aggregation/poll/DefaultPollAggregationProcessor.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 58583f8a91..455ccabbc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.PollSummaryContent import org.matrix.android.sdk.api.session.room.model.VoteInfo import org.matrix.android.sdk.api.session.room.model.VoteSummary -import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper From 637f2476e0dca6d76c0922b3aa2f07d5e853b91a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 12 Dec 2022 11:13:50 +0100 Subject: [PATCH 587/679] Adding changelog entry --- changelog.d/7767.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7767.feature diff --git a/changelog.d/7767.feature b/changelog.d/7767.feature new file mode 100644 index 0000000000..c4386b5e07 --- /dev/null +++ b/changelog.d/7767.feature @@ -0,0 +1 @@ +[Poll] When a poll is ended, use /relations API to ensure poll results are correct From 8c88140b3cc18efdc3fd497e859bdf6b6564b9c8 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 12 Dec 2022 11:40:36 +0100 Subject: [PATCH 588/679] Updating Room API to clarify usage --- .../matrix/android/sdk/internal/session/room/RoomAPI.kt | 9 ++++----- .../session/room/relation/FetchEditHistoryTask.kt | 2 +- .../room/relation/threads/FetchThreadTimelineTask.kt | 6 ++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 4e55b2c40a..f69ee4dc37 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomStrippedState import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams @@ -250,7 +249,7 @@ internal interface RoomAPI { * @param limit max number of Event to retrieve */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") - suspend fun getRelations( + suspend fun getRelationsWithEventType( @Path("roomId") roomId: String, @Path("eventId") eventId: String, @Path("relationType") relationType: String, @@ -261,7 +260,7 @@ internal interface RoomAPI { ): RelationsResponse /** - * Paginate relations for thread events based in normal topological order. + * Paginate relations for events based in normal topological order. * * @param roomId the room Id * @param eventId the event Id @@ -271,10 +270,10 @@ internal interface RoomAPI { * @param limit max number of Event to retrieve */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}") - suspend fun getThreadsRelations( + suspend fun getRelations( @Path("roomId") roomId: String, @Path("eventId") eventId: String, - @Path("relationType") relationType: String = RelationType.THREAD, + @Path("relationType") relationType: String, @Query("from") from: String? = null, @Query("to") to: String? = null, @Query("limit") limit: Int? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt index 93c7f143fd..50439f51eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt @@ -43,7 +43,7 @@ internal class DefaultFetchEditHistoryTask @Inject constructor( override suspend fun execute(params: FetchEditHistoryTask.Params): List { val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) val response = executeRequest(globalErrorReceiver) { - roomAPI.getRelations( + roomAPI.getRelationsWithEventType( roomId = params.roomId, eventId = params.eventId, relationType = RelationType.REPLACE, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index 4cf6445920..1e9a785c80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.DefaultCryptoService @@ -102,11 +103,12 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( override suspend fun execute(params: FetchThreadTimelineTask.Params): Result { val response = executeRequest(globalErrorReceiver) { - roomAPI.getThreadsRelations( + roomAPI.getRelations( roomId = params.roomId, eventId = params.rootThreadEventId, + relationType = RelationType.THREAD, from = params.from, - limit = params.limit + limit = params.limit, ) } From 8b7c8e33519f22e287be3c251edbffb9159b6f9c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 11:26:17 +0100 Subject: [PATCH 589/679] Task to ensure aggregation of all poll responses when receiving ending poll event --- .../sdk/api/session/events/model/Event.kt | 8 +- .../sdk/internal/session/room/RoomModule.kt | 5 + .../poll/DefaultPollAggregationProcessor.kt | 29 +++- .../poll/FetchPollResponseEventsTask.kt | 130 ++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 40ce6ecb5c..9b5f4ac19f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -388,7 +388,13 @@ fun Event.isLocationMessage(): Boolean { } } -fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values +fun Event.isPoll(): Boolean = isPollStart() || isPollEnd() + +fun Event.isPollStart(): Boolean = getClearType() in EventType.POLL_START.values + +fun Event.isPollResponse(): Boolean = getClearType() in EventType.POLL_RESPONSE.values + +fun Event.isPollEnd(): Boolean = getClearType() in EventType.POLL_END.values fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 1475b67276..c28d24995f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -99,6 +99,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask +import org.matrix.android.sdk.internal.session.room.relation.poll.DefaultFetchPollResponseEventsTask +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask @@ -354,4 +356,7 @@ internal abstract class RoomModule { @Binds abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask + + @Binds + abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 455ccabbc6..163d8e5e81 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll import io.realm.Realm +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event @@ -40,9 +41,14 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask +import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject -class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor { +internal class DefaultPollAggregationProcessor @Inject constructor( + private val taskExecutor: TaskExecutor, + private val fetchPollResponseEventsTask: FetchPollResponseEventsTask, +) : PollAggregationProcessor { override fun handlePollStartEvent(realm: Realm, event: Event): Boolean { val content = event.getClearContent()?.toModel() @@ -174,6 +180,10 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) } + if (!isLocalEcho) { + ensurePollIsFullyAggregated(roomId, pollEventId) + } + return true } @@ -200,4 +210,21 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro eventAnnotationsSummaryEntity.pollResponseSummary = it } } + + // TODO add unit tests + /** + * Check that all related votes to a given poll are all retrieved and aggregated. + */ + private fun ensurePollIsFullyAggregated( + roomId: String, + pollEventId: String + ) { + taskExecutor.executorScope.launch { + val params = FetchPollResponseEventsTask.Params( + roomId = roomId, + startPollEventId = pollEventId, + ) + fetchPollResponseEventsTask.execute(params) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt new file mode 100644 index 0000000000..781a0a4d55 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.relation.poll + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.isPollResponse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.EventDecryptor +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock +import javax.inject.Inject + +private const val FETCH_RELATED_EVENTS_LIMIT = 50 + +/** + * Task to fetch all the vote events to ensure full aggregation for a given poll. + */ +internal interface FetchPollResponseEventsTask : Task> { + data class Params( + val roomId: String, + val startPollEventId: String, + ) +} + +// TODO add unit tests +internal class DefaultFetchPollResponseEventsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val globalErrorReceiver: GlobalErrorReceiver, + @SessionDatabase private val monarchy: Monarchy, + private val clock: Clock, + private val eventDecryptor: EventDecryptor, +) : FetchPollResponseEventsTask { + + override suspend fun execute(params: FetchPollResponseEventsTask.Params): Result = runCatching { + var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params) + + while (nextBatch?.isNotEmpty() == true) { + nextBatch = fetchAndProcessRelatedEventsFrom(params, from = nextBatch) + } + } + + private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? { + val response = getRelatedEvents(params, from) + + val filteredEvents = response.chunks + .map { decryptEventIfNeeded(it) } + .filter { it.isPollResponse() } + + addMissingEventsInDB(params.roomId, filteredEvents) + + return response.nextBatch + } + + private suspend fun getRelatedEvents(params: FetchPollResponseEventsTask.Params, from: String? = null): RelationsResponse { + return executeRequest(globalErrorReceiver, canRetry = true) { + roomAPI.getRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = from, + limit = FETCH_RELATED_EVENTS_LIMIT, + ) + } + } + + private suspend fun addMissingEventsInDB(roomId: String, events: List) { + monarchy.awaitTransaction { realm -> + val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } + val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } + + events.filterNot { it.eventId in existingIds } + .map { + val ageLocalTs = clock.epochMillis() - (it.unsignedData?.age ?: 0) + it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = ageLocalTs) + } + .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + } + } + + private suspend fun decryptEventIfNeeded(event: Event): Event { + // TODO move into a reusable task + if (event.isEncrypted()) { + tryOrNull(message = "Unable to decrypt the event") { + eventDecryptor.decryptEvent(event, "") + } + ?.let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe + ) + } + } + + event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) + + return event + } +} From 9338ec9805925f23ce4a065cb0c57ca545bb6545 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 11:49:41 +0100 Subject: [PATCH 590/679] Mutualizing decryption of event --- .../sdk/internal/crypto/EventDecryptor.kt | 23 +++++++++++++++++++ .../poll/FetchPollResponseEventsTask.kt | 16 +------------ .../session/room/timeline/GetEventTask.kt | 13 +---------- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index bc3309132a..c9eabeab48 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -24,10 +24,12 @@ import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent @@ -85,6 +87,27 @@ internal class EventDecryptor @Inject constructor( return internalDecryptEvent(event, timeline) } + /** + * Decrypt an event and save the result in the given event. + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + */ + suspend fun decryptEventAndSaveResult(event: Event, timeline: String) { + tryOrNull(message = "Unable to decrypt the event") { + decryptEvent(event, timeline) + } + ?.let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe + ) + } + } + /** * Decrypt an event asynchronously. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt index 781a0a4d55..3cb8fc5540 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.internal.session.room.relation.poll import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.isPollResponse @@ -107,20 +105,8 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( } private suspend fun decryptEventIfNeeded(event: Event): Event { - // TODO move into a reusable task if (event.isEncrypted()) { - tryOrNull(message = "Unable to decrypt the event") { - eventDecryptor.decryptEvent(event, "") - } - ?.let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } + eventDecryptor.decryptEventAndSaveResult(event, timeline = "") } event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index e0751865ad..c19272f08a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -48,18 +48,7 @@ internal class DefaultGetEventTask @Inject constructor( // Try to decrypt the Event if (event.isEncrypted()) { - tryOrNull(message = "Unable to decrypt the event") { - eventDecryptor.decryptEvent(event, "") - } - ?.let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } + eventDecryptor.decryptEventAndSaveResult(event, timeline = "") } event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) From 644803dcf3615c738d6f6ad77f39cb81a6cc4166 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 14:13:49 +0100 Subject: [PATCH 591/679] Adding unit test on aggregation processor --- .../poll/DefaultPollAggregationProcessor.kt | 1 - .../DefaultPollAggregationProcessorTest.kt | 38 ++++++++++++++++++- .../fakes/FakeFetchPollResponseEventsTask.kt | 22 +++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 163d8e5e81..a424becbd6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -211,7 +211,6 @@ internal class DefaultPollAggregationProcessor @Inject constructor( } } - // TODO add unit tests /** * Check that all related votes to a given poll are all retrieved and aggregated. */ diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index c1fd615e25..6c2fbacd82 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -16,9 +16,13 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.realm.RealmList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.Before @@ -34,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_CONTENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT @@ -43,13 +48,22 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1 +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask +import org.matrix.android.sdk.test.fakes.FakeFetchPollResponseEventsTask import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeTaskExecutor import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenFindFirst +@OptIn(ExperimentalCoroutinesApi::class) class DefaultPollAggregationProcessorTest { - private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() + private val fakeTaskExecutor = FakeTaskExecutor() + private val fakeFetchPollResponseEventsTask = FakeFetchPollResponseEventsTask() + private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor( + taskExecutor = fakeTaskExecutor.instance, + fetchPollResponseEventsTask = fakeFetchPollResponseEventsTask + ) private val realm = FakeRealm() private val session = mockk() @@ -135,6 +149,28 @@ class DefaultPollAggregationProcessorTest { pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse() } + @Test + fun `given a non local echo poll end event, when is processed, then ensure to aggregate all poll responses`() = runTest { + // Given + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", true) + val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id") + every { fakeTaskExecutor.instance.executorScope } returns this + val expectedParams = FetchPollResponseEventsTask.Params( + roomId = A_POLL_END_EVENT.roomId.orEmpty(), + startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(), + ) + + // When + pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event) + advanceUntilIdle() + + // Then + coVerify { + fakeFetchPollResponseEventsTask.execute(expectedParams) + } + } + private fun mockEventAnnotationsSummaryEntity() { realm.givenWhere() .givenFindFirst(EventAnnotationsSummaryEntity()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt new file mode 100644 index 0000000000..575dd4f949 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask + +class FakeFetchPollResponseEventsTask : FetchPollResponseEventsTask by mockk(relaxed = true) From e0a611a16edb25a036296f4763ee3da665c15903 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Wed, 14 Dec 2022 15:13:24 +0100 Subject: [PATCH 592/679] changed copy for threads labs flag (#7776) --- library/ui-strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 0ab1d85f0f..60440b0dd9 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3032,7 +3032,7 @@ Auto Report Decryption Errors. Your system will automatically send logs when an unable to decrypt error occurs - Enable Thread Messages + Enable threaded messages Note: app will be restarted Show latest user info Show the latest profile info (avatar and display name) for all the messages. From bd7b6d6495b1192fbf81ae0ac797354d3650620b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 16:33:27 +0100 Subject: [PATCH 593/679] Adding unit test on task to fetch the poll response events --- .../poll/FetchPollResponseEventsTask.kt | 26 +-- .../DefaultFetchPollResponseEventsTaskTest.kt | 163 ++++++++++++++++++ .../sdk/test/fakes/FakeEventDecryptor.kt | 35 ++++ .../android/sdk/test/fakes/FakeRealm.kt | 8 + .../android/sdk/test/fakes/FakeRoomApi.kt | 61 +++++++ 5 files changed, 281 insertions(+), 12 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt index 3cb8fc5540..3e70da395d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.relation.poll +import androidx.annotation.VisibleForTesting import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.RelationType @@ -37,7 +38,8 @@ import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject -private const val FETCH_RELATED_EVENTS_LIMIT = 50 +@VisibleForTesting +const val FETCH_RELATED_EVENTS_LIMIT = 50 /** * Task to fetch all the vote events to ensure full aggregation for a given poll. @@ -49,7 +51,6 @@ internal interface FetchPollResponseEventsTask : Task) { monarchy.awaitTransaction { realm -> val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } - val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } - - events.filterNot { it.eventId in existingIds } - .map { - val ageLocalTs = clock.epochMillis() - (it.unsignedData?.age ?: 0) - it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = ageLocalTs) - } - .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + if(eventIdsToCheck.isNotEmpty()) { + val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } + + events.filterNot { it.eventId in existingIds } + .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) } + .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + } } } @@ -109,8 +109,10 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( eventDecryptor.decryptEventAndSaveResult(event, timeline = "") } - event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) + event.ageLocalTs = computeLocalTs(event) return event } + + private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt new file mode 100644 index 0000000000..8d50bac38f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.relation.poll + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.isPollResponse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeEventDecryptor +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeRoomApi +import org.matrix.android.sdk.test.fakes.givenFindAll +import org.matrix.android.sdk.test.fakes.givenIn + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultFetchPollResponseEventsTaskTest { + + private val fakeRoomAPI = FakeRoomApi() + private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() + private val fakeMonarchy = FakeMonarchy() + private val fakeClock = FakeClock() + private val fakeEventDecryptor = FakeEventDecryptor() + + private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask( + roomAPI = fakeRoomAPI.instance, + globalErrorReceiver = fakeGlobalErrorReceiver, + monarchy = fakeMonarchy.instance, + clock = fakeClock, + eventDecryptor = fakeEventDecryptor.instance, + ) + + @Before + fun setup() { + mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt") + mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest { + // Given + val aRoomId = "roomId" + val aPollEventId = "eventId" + val params = givenTaskParams(roomId = aRoomId, eventId = aPollEventId) + val aNextBatchToken = "nextBatch" + val anEventId1 = "eventId1" + val anEventId2 = "eventId2" + val anEventId3 = "eventId3" + val anEventId4 = "eventId4" + val event1 = givenAnEvent(eventId = anEventId1, isPollResponse = true, isEncrypted = true) + val event2 = givenAnEvent(eventId = anEventId2, isPollResponse = true, isEncrypted = true) + val event3 = givenAnEvent(eventId = anEventId3, isPollResponse = false, isEncrypted = false) + val event4 = givenAnEvent(eventId = anEventId4, isPollResponse = false, isEncrypted = false) + val firstEvents = listOf(event1, event2) + val secondEvents = listOf(event3, event4) + val firstResponse = givenARelationsResponse(events = firstEvents, nextBatch = aNextBatchToken) + fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse) + val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null) + fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2) + fakeClock.givenEpoch(123) + givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1)) + val eventEntityToSave = EventEntity(eventId = anEventId2) + every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave + every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave + + // When + defaultFetchPollResponseEventsTask.execute(params) + + // Then + fakeRoomAPI.verifyGetRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = null, + limit = FETCH_RELATED_EVENTS_LIMIT + ) + fakeRoomAPI.verifyGetRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = aNextBatchToken, + limit = FETCH_RELATED_EVENTS_LIMIT + ) + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "") + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "") + // Check we save in DB the event2 which is a non stored poll response + verify { + event2.toEntity(aRoomId, SendState.SYNCED, any()) + eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION) + } + } + + private fun givenTaskParams(roomId: String, eventId: String) = FetchPollResponseEventsTask.Params( + roomId = roomId, + startPollEventId = eventId, + ) + + private fun givenARelationsResponse(events: List, nextBatch: String?): RelationsResponse { + return RelationsResponse( + chunks = events, + nextBatch = nextBatch, + prevBatch = null, + ) + } + + private fun givenAnEvent( + eventId: String, + isPollResponse: Boolean, + isEncrypted: Boolean, + ): Event { + val event = mockk(relaxed = true) + every { event.eventId } returns eventId + every { event.isPollResponse() } returns isPollResponse + every { event.isEncrypted() } returns isEncrypted + return event + } + + private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { + val eventEntities = existingIds.map { EventEntity(eventId = it) } + fakeMonarchy.givenWhere() + .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) + .givenFindAll(eventEntities) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt new file mode 100644 index 0000000000..f2b62ad3ba --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.EventDecryptor + +internal class FakeEventDecryptor { + val instance: EventDecryptor = mockk() + + fun givenDecryptEventAndSaveResultSuccess(event: Event) { + coJustRun { instance.decryptEventAndSaveResult(event, any()) } + } + + fun verifyDecryptEventAndSaveResult(event: Event, timeline: String) { + coVerify { instance.decryptEventAndSaveResult(event, timeline) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index afdcf111f8..ba124a86aa 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -109,6 +109,14 @@ inline fun RealmQuery.givenLessThan( return this } +inline fun RealmQuery.givenIn( + fieldName: String, + values: List, +): RealmQuery { + every { `in`(fieldName, values.toTypedArray()) } returns this + return this +} + /** * Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked. */ diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt new file mode 100644 index 0000000000..68dbbe7ea6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse + +internal class FakeRoomApi { + + val instance: RoomAPI = mockk() + + fun givenGetRelationsReturns( + from: String?, + relationsResponse: RelationsResponse, + ) { + coEvery { + instance.getRelations( + roomId = any(), + eventId = any(), + relationType = any(), + from = from, + limit = any() + ) + } returns relationsResponse + } + + fun verifyGetRelations( + roomId: String, + eventId: String, + relationType: String, + from: String?, + limit: Int, + ) { + coVerify { + instance.getRelations( + roomId = roomId, + eventId = eventId, + relationType = relationType, + from = from, + limit = limit + ) + } + } +} From 66abda63ee35bbba6a62f88206dcd3c36737b9c7 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 16:35:34 +0100 Subject: [PATCH 594/679] Removing unused imports --- .../android/sdk/internal/session/room/timeline/GetEventTask.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index c19272f08a..3707205aef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -16,8 +16,6 @@ package org.matrix.android.sdk.internal.session.room.timeline -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.network.GlobalErrorReceiver From dd13e1cb6d8a46556f2dd20d881a90a693ee95e0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 17:02:09 +0100 Subject: [PATCH 595/679] Fixing Copyright in SDK --- .../android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt index 575dd4f949..cb75d8b708 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 4e4f72f241bc097c1e84e79747b508c1f0f63706 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 17:37:45 +0100 Subject: [PATCH 596/679] Fixing code styling issues --- .../session/room/relation/poll/FetchPollResponseEventsTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt index 3e70da395d..e7dd8c57eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -94,7 +94,7 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( private suspend fun addMissingEventsInDB(roomId: String, events: List) { monarchy.awaitTransaction { realm -> val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } - if(eventIdsToCheck.isNotEmpty()) { + if (eventIdsToCheck.isNotEmpty()) { val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } events.filterNot { it.eventId in existingIds } From cf3abd6562f37124e4996cdccbc478dea8bfcd33 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Wed, 14 Dec 2022 18:56:16 +0100 Subject: [PATCH 597/679] thread list loading (#7766) --- changelog.d/5819.misc | 1 + .../org/matrix/android/sdk/flow/FlowRoom.kt | 8 -- .../room/threads/FetchThreadsResult.kt | 23 ++++ .../api/session/room/threads/ThreadFilter.kt | 26 +++++ .../room/threads/ThreadLivePageResult.kt | 27 +++++ .../session/room/threads/ThreadsService.kt | 16 +-- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/helper/ThreadSummaryHelper.kt | 24 ++-- .../database/migration/MigrateSessionTo046.kt | 32 ++++++ .../database/model/SessionRealmModule.kt | 2 + .../model/threads/ThreadListPageEntity.kt | 28 +++++ .../model/threads/ThreadSummaryEntity.kt | 3 + .../query/ThreadSummaryPageEntityQueries.kt | 31 +++++ .../sdk/internal/session/room/RoomAPI.kt | 9 ++ .../threads/FetchThreadSummariesTask.kt | 71 ++++++------ .../threads/ThreadSummariesResponse.kt | 27 +++++ .../room/threads/DefaultThreadsService.kt | 83 ++++++++++---- .../list/viewmodel/ThreadListController.kt | 66 +---------- .../viewmodel/ThreadListPagedController.kt | 84 ++++++++++++++ .../list/viewmodel/ThreadListViewModel.kt | 107 ++++++++++++++---- .../list/viewmodel/ThreadListViewState.kt | 2 - .../threads/list/views/ThreadListFragment.kt | 27 ++++- 22 files changed, 526 insertions(+), 175 deletions(-) create mode 100644 changelog.d/5819.misc create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt diff --git a/changelog.d/5819.misc b/changelog.d/5819.misc new file mode 100644 index 0000000000..5f2d05dc3c --- /dev/null +++ b/changelog.d/5819.misc @@ -0,0 +1 @@ +[Threads] - added API to fetch threads list from the server instead of building it locally from events diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 7ad342b22f..94f09e0bf5 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.send.UserDraft -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional @@ -119,13 +118,6 @@ class FlowRoom(private val room: Room) { return room.roomPushRuleService().getLiveRoomNotificationState().asFlow() } - fun liveThreadSummaries(): Flow> { - return room.threadsService().getAllThreadSummariesLive().asFlow() - .startWith(room.coroutineDispatchers.io) { - room.threadsService().getAllThreadSummaries() - } - } - fun liveThreadList(): Flow> { return room.threadsLocalService().getAllThreadsLive().asFlow() .startWith(room.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt new file mode 100644 index 0000000000..5d4d67a65e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads + +sealed class FetchThreadsResult { + data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult() + object ReachedEnd : FetchThreadsResult() + object Failed : FetchThreadsResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt new file mode 100644 index 0000000000..3f3576728f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class ThreadFilter { + @Json(name = "all") ALL, + @Json(name = "participated") PARTICIPATED, +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt new file mode 100644 index 0000000000..7693dc6fde --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.threads + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary + +data class ThreadLivePageResult( + val livePagedList: LiveData>, + val liveBoundaries: LiveData +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index 9587be68f1..bb6f6b51d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.api.session.room.threads -import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary /** @@ -27,15 +27,14 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary */ interface ThreadsService { - /** - * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level. - */ - fun getAllThreadSummariesLive(): LiveData> + suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult + + suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter = ThreadFilter.ALL): FetchThreadsResult /** * Returns a list of all the [ThreadSummary] that exists at the room level. */ - fun getAllThreadSummaries(): List + suspend fun getAllThreadSummaries(): List /** * Enhance the provided ThreadSummary[List] by adding the latest @@ -51,9 +50,4 @@ interface ThreadsService { * @param limit defines the number of max results the api will respond with */ suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) - - /** - * Fetch all thread summaries for the current room using the enhanced /messages api. - */ - suspend fun fetchThreadSummaries() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 2fb87ca874..5295abffe3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -62,6 +62,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -70,7 +71,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 45L, + schemaVersion = 46L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -125,5 +126,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 43) MigrateSessionTo043(realm).perform() if (oldVersion < 44) MigrateSessionTo044(realm).perform() if (oldVersion < 45) MigrateSessionTo045(realm).perform() + if (oldVersion < 46) MigrateSessionTo046(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 0ac8dc7902..908c710df4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -37,9 +37,11 @@ import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where @@ -113,16 +115,16 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( userId: String, cryptoService: CryptoService? = null, currentTimeMillis: Long, -) { +): ThreadSummaryEntity? { when (threadSummaryType) { ThreadSummaryUpdateType.REPLACE -> { - rootThreadEvent?.eventId ?: return - rootThreadEvent.senderId ?: return + rootThreadEvent?.eventId ?: return null + rootThreadEvent.senderId ?: return null - val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return + val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return null // Something is wrong with the server return - if (numberOfThreads <= 0) return + if (numberOfThreads <= 0) return null val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also { Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") @@ -153,12 +155,13 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( ) roomEntity.addIfNecessary(threadSummary) + return threadSummary } ThreadSummaryUpdateType.ADD -> { - val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return + val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return null Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId") - val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) + var threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) if (threadSummary != null) { // ThreadSummary exists so lets add the latest event Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.") @@ -172,7 +175,7 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one") threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity -> // Root thread event entity exists so lets create a new record - ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let { + threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).also { it.updateThreadSummary( rootThreadEventEntity = rootThreadEventEntity, numberOfThreads = 1, @@ -183,7 +186,12 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( roomEntity.addIfNecessary(it) } } + + threadSummary?.let { + ThreadListPageEntity.get(realm, roomId)?.threadSummaries?.add(it) + } } + return threadSummary } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt new file mode 100644 index 0000000000..4b1d2059a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo046(realm: DynamicRealm) : RealmMigrator(realm, 46) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.create("ThreadListPageEntity") + .addField(ThreadListPageEntityFields.ROOM_ID, String::class.java) + .addPrimaryKey(ThreadListPageEntityFields.ROOM_ID) + .setRequired(ThreadListPageEntityFields.ROOM_ID, true) + .addRealmListField(ThreadListPageEntityFields.THREAD_SUMMARIES.`$`, realm.schema.get("ThreadSummaryEntity")!!) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 93ff67a911..0ab30657ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.annotations.RealmModule import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity /** @@ -72,6 +73,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit UserPresenceEntity::class, ThreadSummaryEntity::class, SyncFilterParamsEntity::class, + ThreadListPageEntity::class ] ) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt new file mode 100644 index 0000000000..1d64c64ddf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model.threads + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ThreadListPageEntity( + @PrimaryKey var roomId: String = "", + var threadSummaries: RealmList = RealmList() +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt index 45f9e3aa20..487be3747a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt @@ -40,5 +40,8 @@ internal open class ThreadSummaryEntity( @LinkingObjects("threadSummaries") val room: RealmResults? = null + @LinkingObjects("threadSummaries") + val page: RealmResults? = null + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt new file mode 100644 index 0000000000..9525e55787 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields + +internal fun ThreadListPageEntity.Companion.get(realm: Realm, roomId: String): ThreadListPageEntity? { + return realm.where().equalTo(ThreadListPageEntityFields.ROOM_ID, roomId).findFirst() +} + +internal fun ThreadListPageEntity.Companion.getOrCreate(realm: Realm, roomId: String): ThreadListPageEntity { + return get(realm, roomId) ?: realm.createObject(roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 4e55b2c40a..ddb7d6a8e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBod import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import org.matrix.android.sdk.internal.session.room.read.ReadBody import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.session.room.relation.threads.ThreadSummariesResponse import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.tags.TagBody @@ -464,4 +465,12 @@ internal interface RoomAPI { @Path("roomIdOrAlias") roomidOrAlias: String, @Query("via") viaServers: List? ): RoomStrippedState + + @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/threads") + suspend fun getThreadsList( + @Path("roomId") roomId: String, + @Query("include") include: String? = "all", + @Query("from") from: String? = null, + @Query("limit") limit: Int? = null + ): ThreadSummariesResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index 254dee4295..848b9698ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -16,37 +16,38 @@ package org.matrix.android.sdk.internal.session.room.relation.threads import com.zhuinden.monarchy.Monarchy +import io.realm.RealmList import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.helper.createOrUpdate import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.filter.FilterFactory import org.matrix.android.sdk.internal.session.room.RoomAPI -import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection -import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber import javax.inject.Inject /*** * This class is responsible to Fetch all the thread in the current room, * To fetch all threads in a room, the /messages API is used with newly added filtering options. */ -internal interface FetchThreadSummariesTask : Task { +internal interface FetchThreadSummariesTask : Task { data class Params( val roomId: String, - val from: String = "", - val limit: Int = 500, - val isUserParticipating: Boolean = true + val from: String? = null, + val limit: Int = 5, + val filter: ThreadFilter? = null, ) } @@ -59,39 +60,43 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( private val clock: Clock, ) : FetchThreadSummariesTask { - override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { - val filter = FilterFactory.createThreadsFilter( - numberOfEvents = params.limit, - userId = if (params.isUserParticipating) userId else null - ).toJSONString() - - val response = executeRequest( - globalErrorReceiver, - canRetry = true - ) { - roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter) + override suspend fun execute(params: FetchThreadSummariesTask.Params): FetchThreadsResult { + val response = executeRequest(globalErrorReceiver) { + roomAPI.getThreadsList( + roomId = params.roomId, + include = params.filter?.toString()?.lowercase(), + from = params.from, + limit = params.limit + ) } - Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ") + handleResponse(response, params) - return handleResponse(response, params) + return when { + response.nextBatch != null -> FetchThreadsResult.ShouldFetchMore(response.nextBatch) + else -> FetchThreadsResult.ReachedEnd + } } private suspend fun handleResponse( - response: PaginationResponse, + response: ThreadSummariesResponse, params: FetchThreadSummariesTask.Params - ): Result { - val rootThreadList = response.events + ) { + val rootThreadList = response.chunk + + val threadSummaries = RealmList() + monarchy.awaitTransaction { realm -> val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction val roomMemberContentsByUser = HashMap() + for (rootThreadEvent in rootThreadList) { if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) { continue } - ThreadSummaryEntity.createOrUpdate( + val threadSummary = ThreadSummaryEntity.createOrUpdate( threadSummaryType = ThreadSummaryUpdateType.REPLACE, realm = realm, roomId = params.roomId, @@ -102,14 +107,16 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( cryptoService = cryptoService, currentTimeMillis = clock.epochMillis(), ) + + threadSummaries.add(threadSummary) } - } - return Result.SUCCESS - } - enum class Result { - SHOULD_FETCH_MORE, - REACHED_END, - SUCCESS + val page = ThreadListPageEntity.getOrCreate(realm, params.roomId) + threadSummaries.forEach { + if (!page.threadSummaries.contains(it)) { + page.threadSummaries.add(it) + } + } + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt new file mode 100644 index 0000000000..d37a058ef6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.internal.session.room.relation.threads + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class ThreadSummariesResponse( + @Json(name = "chunk") val chunk: List, + @Json(name = "next_batch") val nextBatch: String?, + @Json(name = "prev_batch") val prevBatch: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index 6c6d6368d1..63756811f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -16,32 +16,39 @@ package org.matrix.android.sdk.internal.session.room.threads -import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter +import org.matrix.android.sdk.api.session.room.threads.ThreadLivePageResult import org.matrix.android.sdk.api.session.room.threads.ThreadsService import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper -import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultThreadsService @AssistedInject constructor( @Assisted private val roomId: String, - @UserId private val userId: String, private val fetchThreadTimelineTask: FetchThreadTimelineTask, - private val fetchThreadSummariesTask: FetchThreadSummariesTask, @SessionDatabase private val monarchy: Monarchy, - private val timelineEventMapper: TimelineEventMapper, - private val threadSummaryMapper: ThreadSummaryMapper + private val threadSummaryMapper: ThreadSummaryMapper, + private val fetchThreadSummariesTask: FetchThreadSummariesTask, ) : ThreadsService { @AssistedFactory @@ -49,16 +56,58 @@ internal class DefaultThreadsService @AssistedInject constructor( fun create(roomId: String): DefaultThreadsService } - override fun getAllThreadSummariesLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { - threadSummaryMapper.map(it) + override suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult { + monarchy.awaitTransaction { realm -> + realm.where().findAll().deleteAllFromRealm() + } + + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm + .where().equalTo(ThreadSummaryEntityFields.PAGE.ROOM_ID, roomId) + .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING) + } + + val dataSourceFactory = realmDataSourceFactory.map { + threadSummaryMapper.map(it) + } + + val boundaries = MutableLiveData(ResultBoundaries()) + + val builder = LivePagedListBuilder(dataSourceFactory, pagedListConfig).also { + it.setBoundaryCallback(object : PagedList.BoundaryCallback() { + override fun onItemAtEndLoaded(itemAtEnd: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(endLoaded = true)) + } + + override fun onItemAtFrontLoaded(itemAtFront: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(frontLoaded = true)) + } + + override fun onZeroItemsLoaded() { + boundaries.postValue(boundaries.value?.copy(zeroItemLoaded = true)) } + }) + } + + val livePagedList = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + builder ) + return ThreadLivePageResult(livePagedList, boundaries) } - override fun getAllThreadSummaries(): List { + override suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter): FetchThreadsResult { + return fetchThreadSummariesTask.execute( + FetchThreadSummariesTask.Params( + roomId = roomId, + from = nextBatchId, + limit = limit, + filter = filter + ) + ) + } + + override suspend fun getAllThreadSummaries(): List { return monarchy.fetchAllMappedSync( { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { threadSummaryMapper.map(it) } @@ -81,12 +130,4 @@ internal class DefaultThreadsService @AssistedInject constructor( ) ) } - - override suspend fun fetchThreadSummaries() { - fetchThreadSummariesTask.execute( - FetchThreadSummariesTask.Params( - roomId = roomId - ) - ) - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index 14098fd8b0..3cbe652076 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -19,24 +19,18 @@ package im.vector.app.features.home.room.threads.list.viewmodel import com.airbnb.epoxy.EpoxyController import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.threads.list.model.threadListItem -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.api.util.toMatrixItemOrNull import javax.inject.Inject class ThreadListController @Inject constructor( private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, private val displayableEventFormatter: DisplayableEventFormatter, - private val session: Session ) : EpoxyController() { var listener: Listener? = null @@ -48,64 +42,7 @@ class ThreadListController @Inject constructor( requestModelBuild() } - override fun buildModels() = - when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { - true -> buildThreadSummaries() - false -> buildThreadList() - } - - /** - * Building thread summaries when homeserver supports threading. - */ - private fun buildThreadSummaries() { - val safeViewState = viewState ?: return - val host = this - safeViewState.threadSummaryList.invoke() - ?.filter { - if (safeViewState.shouldFilterThreads) { - it.isUserParticipating - } else { - true - } - } - ?.forEach { threadSummary -> - val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) - val lastMessageFormatted = threadSummary.let { - displayableEventFormatter.formatThreadSummary( - event = it.latestEvent, - latestEdition = it.threadEditions.latestThreadEdition - ).toString() - } - val rootMessageFormatted = threadSummary.let { - displayableEventFormatter.formatThreadSummary( - event = it.rootEvent, - latestEdition = it.threadEditions.rootThreadEdition - ).toString() - } - threadListItem { - id(threadSummary.rootEvent?.eventId) - avatarRenderer(host.avatarRenderer) - matrixItem(threadSummary.rootThreadSenderInfo.toMatrixItem()) - title(threadSummary.rootThreadSenderInfo.displayName.orEmpty()) - date(date) - rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false) - // TODO refactor notifications that with the new thread summary - threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(rootMessageFormatted) - lastMessage(lastMessageFormatted) - lastMessageCounter(threadSummary.numberOfThreads.toString()) - lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull()) - itemClickListener { - host.listener?.onThreadSummaryClicked(threadSummary) - } - } - } - } - - /** - * Building local thread list when homeserver do not support threading. - */ - private fun buildThreadList() { + override fun buildModels() { val safeViewState = viewState ?: return val host = this safeViewState.rootThreadEventList.invoke() @@ -152,7 +89,6 @@ class ThreadListController @Inject constructor( } interface Listener { - fun onThreadSummaryClicked(threadSummary: ThreadSummary) fun onThreadListClicked(timelineEvent: TimelineEvent) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt new file mode 100644 index 0000000000..171b690a33 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.threads.list.viewmodel + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.utils.createUIHandler +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter +import im.vector.app.features.home.room.threads.list.model.ThreadListItem_ +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.util.toMatrixItemOrNull +import javax.inject.Inject + +class ThreadListPagedController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val dateFormatter: VectorDateFormatter, + private val displayableEventFormatter: DisplayableEventFormatter, +) : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + var listener: Listener? = null + + override fun buildItemModel(currentPosition: Int, item: ThreadSummary?): EpoxyModel<*> { + if (item == null) { + throw java.lang.NullPointerException() + } + val host = this + val date = dateFormatter.format(item.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) + val lastMessageFormatted = item.let { + displayableEventFormatter.formatThreadSummary( + event = it.latestEvent, + latestEdition = it.threadEditions.latestThreadEdition + ).toString() + } + val rootMessageFormatted = item.let { + displayableEventFormatter.formatThreadSummary( + event = it.rootEvent, + latestEdition = it.threadEditions.rootThreadEdition + ).toString() + } + + return ThreadListItem_() + .id(item.rootEvent?.eventId) + .avatarRenderer(host.avatarRenderer) + .matrixItem(item.rootThreadSenderInfo.toMatrixItem()) + .title(item.rootThreadSenderInfo.displayName.orEmpty()) + .date(date) + .rootMessageDeleted(item.rootEvent?.isRedacted() ?: false) + // TODO refactor notifications that with the new thread summary + .threadNotificationState(item.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) + .rootMessage(rootMessageFormatted) + .lastMessage(lastMessageFormatted) + .lastMessageCounter(item.numberOfThreads.toString()) + .lastMessageMatrixItem(item.latestThreadSenderInfo.toMatrixItemOrNull()) + .itemClickListener { + host.listener?.onThreadSummaryClicked(item) + } + } + + interface Listener { + fun onThreadSummaryClicked(threadSummary: ThreadSummary) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 4b7af330fb..7124727bb7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -16,6 +16,11 @@ package im.vector.app.features.home.room.threads.list.viewmodel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.asFlow +import androidx.paging.PagedList import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -29,23 +34,47 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.room.threads.list.views.ThreadListFragment +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.flow.flow class ThreadListViewModel @AssistedInject constructor( @Assisted val initialState: ThreadListViewState, private val analyticsTracker: AnalyticsTracker, - private val session: Session -) : - VectorViewModel(initialState) { + private val session: Session, +) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId) + private val defaultPagedListConfig = PagedList.Config.Builder() + .setPageSize(20) + .setInitialLoadSizeHint(40) + .setEnablePlaceholders(false) + .setPrefetchDistance(10) + .build() + + private var nextBatchId: String? = null + private var hasReachedEnd: Boolean = false + private var boundariesJob: Job? = null + + private var livePagedList: LiveData>? = null + private val _threadsLivePagedList = MutableLiveData>() + val threadsLivePagedList: LiveData> = _threadsLivePagedList + private val internalPagedListObserver = Observer> { + _threadsLivePagedList.postValue(it) + setLoading(false) + } + @AssistedFactory interface Factory { fun create(initialState: ThreadListViewState): ThreadListViewModel @@ -54,7 +83,7 @@ class ThreadListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? { + override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel { val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.threadListViewModelFactory.create(state) } @@ -72,7 +101,7 @@ class ThreadListViewModel @AssistedInject constructor( private fun fetchAndObserveThreads() { when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { true -> { - fetchThreadList() + setLoading(true) observeThreadSummaries() } false -> observeThreadsList() @@ -82,14 +111,33 @@ class ThreadListViewModel @AssistedInject constructor( /** * Observing thread summaries when homeserver support threading. */ - private fun observeThreadSummaries() { - room?.flow() - ?.liveThreadSummaries() - ?.map { room.threadsService().enhanceThreadWithEditions(it) } - ?.flowOn(room.coroutineDispatchers.io) - ?.execute { asyncThreads -> - copy(threadSummaryList = asyncThreads) - } + private fun observeThreadSummaries() = withState { state -> + viewModelScope.launch { + nextBatchId = null + hasReachedEnd = false + + livePagedList?.removeObserver(internalPagedListObserver) + + room?.threadsService() + ?.getPagedThreadsList(state.shouldFilterThreads, defaultPagedListConfig)?.let { result -> + livePagedList = result.livePagedList + + livePagedList?.observeForever(internalPagedListObserver) + + boundariesJob = result.liveBoundaries.asFlow() + .onEach { + if (it.endLoaded) { + if (!hasReachedEnd) { + fetchNextPage() + } + } + } + .launchIn(viewModelScope) + } + + setLoading(true) + fetchNextPage() + } } /** @@ -111,14 +159,6 @@ class ThreadListViewModel @AssistedInject constructor( } } - private fun fetchThreadList() { - viewModelScope.launch { - setLoading(true) - room?.threadsService()?.fetchThreadSummaries() - setLoading(false) - } - } - private fun setLoading(isLoading: Boolean) { setState { copy(isLoading = isLoading) @@ -132,5 +172,30 @@ class ThreadListViewModel @AssistedInject constructor( setState { copy(shouldFilterThreads = shouldFilterThreads) } + + fetchAndObserveThreads() + } + + private suspend fun fetchNextPage() { + val filter = when (awaitState().shouldFilterThreads) { + true -> ThreadFilter.PARTICIPATED + false -> ThreadFilter.ALL + } + room?.threadsService()?.fetchThreadList( + nextBatchId = nextBatchId, + limit = defaultPagedListConfig.pageSize, + filter = filter, + ).let { result -> + when (result) { + is FetchThreadsResult.ReachedEnd -> { + hasReachedEnd = true + } + is FetchThreadsResult.ShouldFetchMore -> { + nextBatchId = result.nextBatch + } + else -> { + } + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt index 2328da0b8a..60ccfb59af 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt @@ -20,11 +20,9 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.home.room.threads.arguments.ThreadListArgs -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent data class ThreadListViewState( - val threadSummaryList: Async> = Uninitialized, val rootThreadEventList: Async> = Uninitialized, val shouldFilterThreads: Boolean = false, val isLoading: Boolean = false, diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index f91fe9bd91..318c250906 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -40,6 +40,7 @@ import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListPagedController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.rageshake.BugReporter @@ -52,12 +53,14 @@ import javax.inject.Inject @AndroidEntryPoint class ThreadListFragment : VectorBaseFragment(), + ThreadListPagedController.Listener, ThreadListController.Listener, VectorMenuProvider { @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var bugReporter: BugReporter - @Inject lateinit var threadListController: ThreadListController + @Inject lateinit var threadListController: ThreadListPagedController + @Inject lateinit var legacyThreadListController: ThreadListController @Inject lateinit var threadListViewModelFactory: ThreadListViewModel.Factory private val threadListViewModel: ThreadListViewModel by fragmentViewModel() @@ -100,7 +103,7 @@ class ThreadListFragment : val filterBadge = filterIcon.findViewById(R.id.threadListFilterBadge) filterBadge.isVisible = state.shouldFilterThreads when (threadListViewModel.canHomeserverUseThreading()) { - true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.threadSummaryList.invoke().isNullOrEmpty() + true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = true false -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.rootThreadEventList.invoke().isNullOrEmpty() } } @@ -111,8 +114,18 @@ class ThreadListFragment : initToolbar() initTextConstants() initBetaFeedback() - views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false) - threadListController.listener = this + + if (threadListViewModel.canHomeserverUseThreading()) { + views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false) + threadListController.listener = this + + threadListViewModel.threadsLivePagedList.observe(viewLifecycleOwner) { threadsList -> + threadListController.submitList(threadsList) + } + } else { + views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false) + legacyThreadListController.listener = this + } } override fun onDestroyView() { @@ -144,7 +157,9 @@ class ThreadListFragment : override fun invalidate() = withState(threadListViewModel) { state -> invalidateOptionsMenu() renderEmptyStateIfNeeded(state) - threadListController.update(state) + if (!threadListViewModel.canHomeserverUseThreading()) { + legacyThreadListController.update(state) + } renderLoaderIfNeeded(state) } @@ -185,7 +200,7 @@ class ThreadListFragment : private fun renderEmptyStateIfNeeded(state: ThreadListViewState) { when (threadListViewModel.canHomeserverUseThreading()) { - true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty() + true -> views.threadListEmptyConstraintLayout.isVisible = false false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty() } } From e5663ec1c3274d3e0e827414ca2d57711ee25c8d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 09:45:17 +0100 Subject: [PATCH 598/679] Fixing unit tests --- .../DefaultPollAggregationProcessorTest.kt | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index 6c2fbacd82..0888d82907 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -128,16 +128,28 @@ class DefaultPollAggregationProcessorTest { } @Test - fun `given a poll end event, when processing, then is processed and return true`() { + fun `given a poll end event, when processing, then is processed and return true`() = runTest { + // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + every { fakeTaskExecutor.instance.executorScope } returns this + + // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) + + // Then pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() } @Test - fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() { + fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() = runTest { + // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + every { fakeTaskExecutor.instance.executorScope } returns this + + // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) + + // Then pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() } @@ -157,8 +169,8 @@ class DefaultPollAggregationProcessorTest { val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id") every { fakeTaskExecutor.instance.executorScope } returns this val expectedParams = FetchPollResponseEventsTask.Params( - roomId = A_POLL_END_EVENT.roomId.orEmpty(), - startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(), + roomId = A_POLL_END_EVENT.roomId.orEmpty(), + startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(), ) // When From 82ad08aced2979b6c37459dd6d965260a80f23dc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Dec 2022 10:15:24 +0100 Subject: [PATCH 599/679] Changelog for version 1.5.12 --- CHANGES.md | 51 ++++++++++++++++++++++++++++++++++++++++ changelog.d/5503.misc | 1 - changelog.d/5819.misc | 1 - changelog.d/7274.bugfix | 1 - changelog.d/7477.misc | 1 - changelog.d/7596.feature | 1 - changelog.d/7632.feature | 1 - changelog.d/7643.bugfix | 1 - changelog.d/7645.misc | 1 - changelog.d/7653.bugfix | 1 - changelog.d/7658.bugfix | 1 - changelog.d/7659.bugfix | 1 - changelog.d/7680.bugfix | 3 --- changelog.d/7683.bugfix | 2 -- changelog.d/7684.bugfix | 1 - changelog.d/7691.bugfix | 1 - changelog.d/7693.feature | 1 - changelog.d/7694.feature | 1 - changelog.d/7695.bugfix | 1 - changelog.d/7697.feature | 1 - changelog.d/7699.bugfix | 1 - changelog.d/7708.misc | 1 - changelog.d/7710.bugfix | 1 - changelog.d/7719.feature | 1 - changelog.d/7723.misc | 1 - changelog.d/7725.bugfix | 1 - changelog.d/7733.bugfix | 1 - changelog.d/7737.bugfix | 1 - changelog.d/7740.feature | 1 - changelog.d/7743.bugfix | 1 - changelog.d/7744.bugfix | 1 - changelog.d/7751.bugfix | 1 - changelog.d/7753.bugfix | 1 - changelog.d/7754.feature | 1 - changelog.d/7770.bugfix | 1 - 35 files changed, 51 insertions(+), 37 deletions(-) delete mode 100644 changelog.d/5503.misc delete mode 100644 changelog.d/5819.misc delete mode 100644 changelog.d/7274.bugfix delete mode 100644 changelog.d/7477.misc delete mode 100644 changelog.d/7596.feature delete mode 100644 changelog.d/7632.feature delete mode 100644 changelog.d/7643.bugfix delete mode 100644 changelog.d/7645.misc delete mode 100644 changelog.d/7653.bugfix delete mode 100644 changelog.d/7658.bugfix delete mode 100644 changelog.d/7659.bugfix delete mode 100644 changelog.d/7680.bugfix delete mode 100644 changelog.d/7683.bugfix delete mode 100644 changelog.d/7684.bugfix delete mode 100644 changelog.d/7691.bugfix delete mode 100644 changelog.d/7693.feature delete mode 100644 changelog.d/7694.feature delete mode 100644 changelog.d/7695.bugfix delete mode 100644 changelog.d/7697.feature delete mode 100644 changelog.d/7699.bugfix delete mode 100644 changelog.d/7708.misc delete mode 100644 changelog.d/7710.bugfix delete mode 100644 changelog.d/7719.feature delete mode 100644 changelog.d/7723.misc delete mode 100644 changelog.d/7725.bugfix delete mode 100644 changelog.d/7733.bugfix delete mode 100644 changelog.d/7737.bugfix delete mode 100644 changelog.d/7740.feature delete mode 100644 changelog.d/7743.bugfix delete mode 100644 changelog.d/7744.bugfix delete mode 100644 changelog.d/7751.bugfix delete mode 100644 changelog.d/7753.bugfix delete mode 100644 changelog.d/7754.feature delete mode 100644 changelog.d/7770.bugfix diff --git a/CHANGES.md b/CHANGES.md index c170c3b92b..0481ec1af6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,54 @@ +Changes in Element v1.5.12 (2022-12-15) +======================================= + +Features ✨ +---------- +- [Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually ([#5503](https://github.com/vector-im/element-android/issues/5503)) + - [Session manager] Add action to signout all the other session ([#7693](https://github.com/vector-im/element-android/issues/7693)) + - Remind unverified sessions with a banner once a week ([#7694](https://github.com/vector-im/element-android/issues/7694)) + - [Session manager] Add actions to rename and signout current session ([#7697](https://github.com/vector-im/element-android/issues/7697)) + - Voice Broadcast - Update last message in the room list ([#7719](https://github.com/vector-im/element-android/issues/7719)) + - Delete unused client information from account data ([#7754](https://github.com/vector-im/element-android/issues/7754)) + +Bugfixes 🐛 +---------- + - Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) ([#7274](https://github.com/vector-im/element-android/issues/7274)) + - [Notifications] Fixed a bug when push notification was automatically dismissed while app is on background ([#7643](https://github.com/vector-im/element-android/issues/7643)) + - ANR when asking to select the notification method ([#7653](https://github.com/vector-im/element-android/issues/7653)) + - [Rich text editor] Fix design and spacing of rich text editor ([#7658](https://github.com/vector-im/element-android/issues/7658)) + - [Rich text editor] Fix keyboard closing after collapsing editor ([#7659](https://github.com/vector-im/element-android/issues/7659)) + - Rich Text Editor: fix several issues related to insets: + * Empty space displayed at the bottom when you don't have permissions to send messages into a room. + * Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. ([#7680](https://github.com/vector-im/element-android/issues/7680)) + - Fix crash in message composer when room is missing ([#7683](https://github.com/vector-im/element-android/issues/7683)) + - Fix crash when invalid homeserver url is entered. ([#7684](https://github.com/vector-im/element-android/issues/7684)) + - Rich Text Editor: improve performance when entering reply/edit/quote mode. ([#7691](https://github.com/vector-im/element-android/issues/7691)) + - [Rich text editor] Add error tracking for rich text editor ([#7695](https://github.com/vector-im/element-android/issues/7695)) + - Fix E2EE set up failure whilst signing in using QR code ([#7699](https://github.com/vector-im/element-android/issues/7699)) + - Fix usage of unknown shield in room summary ([#7710](https://github.com/vector-im/element-android/issues/7710)) + - Fix crash when the network is not available. ([#7725](https://github.com/vector-im/element-android/issues/7725)) + - [Session manager] Sessions without encryption support should not prompt to verify ([#7733](https://github.com/vector-im/element-android/issues/7733)) + - Fix issue of Scan QR code button sometimes not showing when it should be available ([#7737](https://github.com/vector-im/element-android/issues/7737)) + - Verification request is not showing when verify session popup is displayed ([#7743](https://github.com/vector-im/element-android/issues/7743)) + - Fix crash when inviting by email. ([#7744](https://github.com/vector-im/element-android/issues/7744)) + - Revert usage of stable fields in live location sharing and polls ([#7751](https://github.com/vector-im/element-android/issues/7751)) + - [Poll] Poll end event is not recognized ([#7753](https://github.com/vector-im/element-android/issues/7753)) + - [Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline ([#7770](https://github.com/vector-im/element-android/issues/7770)) + +Other changes +------------- + - [Threads] - added API to fetch threads list from the server instead of building it locally from events ([#5819](https://github.com/vector-im/element-android/issues/5819)) + - Add Z-Labs label for rich text editor and migrate to new label naming. ([#7477](https://github.com/vector-im/element-android/issues/7477)) + - Crypto database migration tests ([#7645](https://github.com/vector-im/element-android/issues/7645)) + - Add tracing Id for to device messages ([#7708](https://github.com/vector-im/element-android/issues/7708)) + - Disable nightly popup and add an entry point in the advanced settings instead. ([#7723](https://github.com/vector-im/element-android/issues/7723)) +- Save m.local_notification_settings. event in account_data ([#7596](https://github.com/vector-im/element-android/issues/7596)) +- Update notifications setting when m.local_notification_settings. event changes for current device ([#7632](https://github.com/vector-im/element-android/issues/7632)) + +SDK API changes ⚠️ +------------------ +- Handle account data removal ([#7740](https://github.com/vector-im/element-android/issues/7740)) + Changes in Element 1.5.11 (2022-12-07) ====================================== diff --git a/changelog.d/5503.misc b/changelog.d/5503.misc deleted file mode 100644 index 66deb33684..0000000000 --- a/changelog.d/5503.misc +++ /dev/null @@ -1 +0,0 @@ -[Threads] - Threads Labs Flag is enabled by default and forced to be enabled for existing users, but sill can be disabled manually diff --git a/changelog.d/5819.misc b/changelog.d/5819.misc deleted file mode 100644 index 5f2d05dc3c..0000000000 --- a/changelog.d/5819.misc +++ /dev/null @@ -1 +0,0 @@ -[Threads] - added API to fetch threads list from the server instead of building it locally from events diff --git a/changelog.d/7274.bugfix b/changelog.d/7274.bugfix deleted file mode 100644 index e99daceb89..0000000000 --- a/changelog.d/7274.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) diff --git a/changelog.d/7477.misc b/changelog.d/7477.misc deleted file mode 100644 index 2ea83ce81d..0000000000 --- a/changelog.d/7477.misc +++ /dev/null @@ -1 +0,0 @@ -Add Z-Labs label for rich text editor and migrate to new label naming. \ No newline at end of file diff --git a/changelog.d/7596.feature b/changelog.d/7596.feature deleted file mode 100644 index 022d86342b..0000000000 --- a/changelog.d/7596.feature +++ /dev/null @@ -1 +0,0 @@ -Save m.local_notification_settings. event in account_data diff --git a/changelog.d/7632.feature b/changelog.d/7632.feature deleted file mode 100644 index 460f987756..0000000000 --- a/changelog.d/7632.feature +++ /dev/null @@ -1 +0,0 @@ -Update notifications setting when m.local_notification_settings. event changes for current device diff --git a/changelog.d/7643.bugfix b/changelog.d/7643.bugfix deleted file mode 100644 index 66e3f28d5f..0000000000 --- a/changelog.d/7643.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Notifications] Fixed a bug when push notification was automatically dismissed while app is on background diff --git a/changelog.d/7645.misc b/changelog.d/7645.misc deleted file mode 100644 index a133581ac1..0000000000 --- a/changelog.d/7645.misc +++ /dev/null @@ -1 +0,0 @@ -Crypto database migration tests diff --git a/changelog.d/7653.bugfix b/changelog.d/7653.bugfix deleted file mode 100644 index ae49c4ed4e..0000000000 --- a/changelog.d/7653.bugfix +++ /dev/null @@ -1 +0,0 @@ -ANR when asking to select the notification method diff --git a/changelog.d/7658.bugfix b/changelog.d/7658.bugfix deleted file mode 100644 index a5ab85b191..0000000000 --- a/changelog.d/7658.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Fix design and spacing of rich text editor diff --git a/changelog.d/7659.bugfix b/changelog.d/7659.bugfix deleted file mode 100644 index 38be1008ef..0000000000 --- a/changelog.d/7659.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Fix keyboard closing after collapsing editor diff --git a/changelog.d/7680.bugfix b/changelog.d/7680.bugfix deleted file mode 100644 index 2e3b4b2e48..0000000000 --- a/changelog.d/7680.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Rich Text Editor: fix several issues related to insets: -* Empty space displayed at the bottom when you don't have permissions to send messages into a room. -* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. diff --git a/changelog.d/7683.bugfix b/changelog.d/7683.bugfix deleted file mode 100644 index 3922253ba6..0000000000 --- a/changelog.d/7683.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix crash in message composer when room is missing - diff --git a/changelog.d/7684.bugfix b/changelog.d/7684.bugfix deleted file mode 100644 index 4a9af884a1..0000000000 --- a/changelog.d/7684.bugfix +++ /dev/null @@ -1 +0,0 @@ - Fix crash when invalid homeserver url is entered. diff --git a/changelog.d/7691.bugfix b/changelog.d/7691.bugfix deleted file mode 100644 index 0298819143..0000000000 --- a/changelog.d/7691.bugfix +++ /dev/null @@ -1 +0,0 @@ -Rich Text Editor: improve performance when entering reply/edit/quote mode. diff --git a/changelog.d/7693.feature b/changelog.d/7693.feature deleted file mode 100644 index 271964db82..0000000000 --- a/changelog.d/7693.feature +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Add action to signout all the other session diff --git a/changelog.d/7694.feature b/changelog.d/7694.feature deleted file mode 100644 index 408925974e..0000000000 --- a/changelog.d/7694.feature +++ /dev/null @@ -1 +0,0 @@ -Remind unverified sessions with a banner once a week diff --git a/changelog.d/7695.bugfix b/changelog.d/7695.bugfix deleted file mode 100644 index 7ec0805bce..0000000000 --- a/changelog.d/7695.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Rich text editor] Add error tracking for rich text editor diff --git a/changelog.d/7697.feature b/changelog.d/7697.feature deleted file mode 100644 index 6d71a84a40..0000000000 --- a/changelog.d/7697.feature +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Add actions to rename and signout current session diff --git a/changelog.d/7699.bugfix b/changelog.d/7699.bugfix deleted file mode 100644 index 30a4b8e9fa..0000000000 --- a/changelog.d/7699.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix E2EE set up failure whilst signing in using QR code diff --git a/changelog.d/7708.misc b/changelog.d/7708.misc deleted file mode 100644 index 6273330395..0000000000 --- a/changelog.d/7708.misc +++ /dev/null @@ -1 +0,0 @@ -Add tracing Id for to device messages diff --git a/changelog.d/7710.bugfix b/changelog.d/7710.bugfix deleted file mode 100644 index 9e75a03e1b..0000000000 --- a/changelog.d/7710.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix usage of unknown shield in room summary diff --git a/changelog.d/7719.feature b/changelog.d/7719.feature deleted file mode 100644 index 34df6ad964..0000000000 --- a/changelog.d/7719.feature +++ /dev/null @@ -1 +0,0 @@ -Voice Broadcast - Update last message in the room list diff --git a/changelog.d/7723.misc b/changelog.d/7723.misc deleted file mode 100644 index 36869d1efb..0000000000 --- a/changelog.d/7723.misc +++ /dev/null @@ -1 +0,0 @@ -Disable nightly popup and add an entry point in the advanced settings instead. diff --git a/changelog.d/7725.bugfix b/changelog.d/7725.bugfix deleted file mode 100644 index b701451505..0000000000 --- a/changelog.d/7725.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash when the network is not available. diff --git a/changelog.d/7733.bugfix b/changelog.d/7733.bugfix deleted file mode 100644 index 9de3759f1a..0000000000 --- a/changelog.d/7733.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Session manager] Sessions without encryption support should not prompt to verify diff --git a/changelog.d/7737.bugfix b/changelog.d/7737.bugfix deleted file mode 100644 index 1477834674..0000000000 --- a/changelog.d/7737.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix issue of Scan QR code button sometimes not showing when it should be available diff --git a/changelog.d/7740.feature b/changelog.d/7740.feature deleted file mode 100644 index 6cd2b6c776..0000000000 --- a/changelog.d/7740.feature +++ /dev/null @@ -1 +0,0 @@ -Handle account data removal diff --git a/changelog.d/7743.bugfix b/changelog.d/7743.bugfix deleted file mode 100644 index 867c12a3c3..0000000000 --- a/changelog.d/7743.bugfix +++ /dev/null @@ -1 +0,0 @@ -Verification request is not showing when verify session popup is displayed diff --git a/changelog.d/7744.bugfix b/changelog.d/7744.bugfix deleted file mode 100644 index 7ed82a9c1c..0000000000 --- a/changelog.d/7744.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix crash when inviting by email. diff --git a/changelog.d/7751.bugfix b/changelog.d/7751.bugfix deleted file mode 100644 index 5d676dbc4d..0000000000 --- a/changelog.d/7751.bugfix +++ /dev/null @@ -1 +0,0 @@ -Revert usage of stable fields in live location sharing and polls diff --git a/changelog.d/7753.bugfix b/changelog.d/7753.bugfix deleted file mode 100644 index 10579b6a84..0000000000 --- a/changelog.d/7753.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Poll] Poll end event is not recognized diff --git a/changelog.d/7754.feature b/changelog.d/7754.feature deleted file mode 100644 index 0e1b6d0961..0000000000 --- a/changelog.d/7754.feature +++ /dev/null @@ -1 +0,0 @@ -Delete unused client information from account data diff --git a/changelog.d/7770.bugfix b/changelog.d/7770.bugfix deleted file mode 100644 index 598deb6073..0000000000 --- a/changelog.d/7770.bugfix +++ /dev/null @@ -1 +0,0 @@ -[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline From dd88ac597e60d9011e603116d2796e5a505a2e74 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Dec 2022 10:16:02 +0100 Subject: [PATCH 600/679] Adding fastlane file for version 1.5.12 --- fastlane/metadata/android/en-US/changelogs/40105120.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40105120.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40105120.txt b/fastlane/metadata/android/en-US/changelogs/40105120.txt new file mode 100644 index 0000000000..91c25cf053 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105120.txt @@ -0,0 +1,2 @@ +Main changes in this version: Thread are now enabled by default. +Full changelog: https://github.com/vector-im/element-android/releases From 8c49609aa635a0bb0e73e8d0dad82b2e3e5e41dd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Dec 2022 10:20:26 +0100 Subject: [PATCH 601/679] version++ --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 4be8d55614..4558f4e8b5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.12\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.14\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 1df5608871..7d1073764d 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 12 +ext.versionPatch = 14 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From cf98963cdbfc5173bf34a996d188eb4c0f48e9eb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 10:41:44 +0100 Subject: [PATCH 602/679] Adding changelog entry --- changelog.d/7784.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7784.bugfix diff --git a/changelog.d/7784.bugfix b/changelog.d/7784.bugfix new file mode 100644 index 0000000000..107da01877 --- /dev/null +++ b/changelog.d/7784.bugfix @@ -0,0 +1 @@ +[Session manager] Other sessions list: filter option is displayed when selection mode is enabled From dcb8aea29252481ae183926013668427405e4fc3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 11:02:59 +0100 Subject: [PATCH 603/679] Hiding the filter icon in top bar when in selection mode --- .../devices/v2/othersessions/OtherSessionsFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 87330b087a..be87645ea6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -225,6 +225,7 @@ class OtherSessionsFragment : override fun invalidate() = withState(viewModel) { state -> updateLoading(state.isLoading) + updateFilterView(state.isSelectModeEnabled) if (state.devices is Success) { val devices = state.devices.invoke() renderDevices(devices, state.currentFilter, state.isShowingIpAddress) @@ -240,6 +241,10 @@ class OtherSessionsFragment : } } + private fun updateFilterView(isSelectModeEnabled: Boolean) { + views.otherSessionsFilterFrameLayout.isVisible = isSelectModeEnabled.not() + } + private fun updateToolbar(devices: List, isSelectModeEnabled: Boolean) { invalidateOptionsMenu() val title = if (isSelectModeEnabled) { From 9736a8f5710e3304029e13dea3fbe5a8e5c51fe7 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 11:26:12 +0100 Subject: [PATCH 604/679] Adding changelog entry --- changelog.d/7786.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7786.bugfix diff --git a/changelog.d/7786.bugfix b/changelog.d/7786.bugfix new file mode 100644 index 0000000000..60a4a324d4 --- /dev/null +++ b/changelog.d/7786.bugfix @@ -0,0 +1 @@ +[Session manager] Other sessions: Filter bottom sheet cut in landscape mode From cc33c008ba20fc93f53edfe49b0093a8332cd203 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 15 Dec 2022 15:50:02 +0300 Subject: [PATCH 605/679] Automatically show keyboard after learn more bottom sheet is dismissed. --- .../v2/more/SessionLearnMoreBottomSheet.kt | 11 ++++++++++- .../devices/v2/rename/RenameSessionFragment.kt | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt index 22ca06eb1e..502d9abca3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.more +import android.content.DialogInterface import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -42,6 +43,8 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment Unit)? = null + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding { return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false) } @@ -57,6 +60,11 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment super.invalidate() views.bottomSheetSessionLearnMoreTitle.text = viewState.title @@ -65,11 +73,12 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment viewModel.handle(RenameSessionAction.EditLocally(text.toString())) } } + private fun showKeyboard() { + views.renameSessionEditText.viewTreeObserver.addOnWindowFocusChangeListener { hasFocus -> + if (hasFocus) { + views.renameSessionEditText.showKeyboard(andRequestFocus = true) + } + } + } + private fun initSaveButton() { views.renameSessionSave.debouncedClicks { viewModel.handle(RenameSessionAction.SaveModifications) @@ -89,7 +97,13 @@ class RenameSessionFragment : title = getString(R.string.device_manager_learn_more_session_rename_title), description = getString(R.string.device_manager_learn_more_session_rename), ) - SessionLearnMoreBottomSheet.show(childFragmentManager, args) + SessionLearnMoreBottomSheet + .show(childFragmentManager, args) + .apply { + onDismiss = { + showKeyboard() + } + } } private fun observeViewEvents() { From 6d40bd157fb7000b012b31602a9fe27881844ddf Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 15 Dec 2022 16:00:03 +0300 Subject: [PATCH 606/679] Add changelog. --- changelog.d/7790.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7790.bugfix diff --git a/changelog.d/7790.bugfix b/changelog.d/7790.bugfix new file mode 100644 index 0000000000..7390f92b32 --- /dev/null +++ b/changelog.d/7790.bugfix @@ -0,0 +1 @@ +Automatically show keyboard after learn more bottom sheet is dismissed From a86f2e03cca9ecbeb6d688dcaa1976bd8c6ea5c9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 14:14:48 +0100 Subject: [PATCH 607/679] Make the radiogroup scrollable to better support landscape on small devices --- .../bottom_sheet_device_manager_filter.xml | 120 +++++++++--------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml index a7987e70b5..56421532b5 100644 --- a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml +++ b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml @@ -20,73 +20,79 @@ android:layout_marginTop="12dp" android:text="@string/device_manager_filter_bottom_sheet_title" /> - - - + android:layout_height="match_parent"> - + android:layoutDirection="rtl" + android:showDividers="none"> - + - + - + - + - + + + + + + + - + From 9178426ec19564e8dec8c0e90f3fe7dc4ead57f4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 14:31:46 +0100 Subject: [PATCH 608/679] Adding changelog entry --- changelog.d/7792.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7792.bugfix diff --git a/changelog.d/7792.bugfix b/changelog.d/7792.bugfix new file mode 100644 index 0000000000..d5c80a0825 --- /dev/null +++ b/changelog.d/7792.bugfix @@ -0,0 +1 @@ +[Session Manager] Other sessions list: cannot select/deselect session by a long press when in select mode From 67edf6685644230e2f5b6e877071b88b92f6a63a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 14:34:55 +0100 Subject: [PATCH 609/679] Bump sentry-android from 6.9.0 to 6.9.2 (#7731) Bumps [sentry-android](https://github.com/getsentry/sentry-java) from 6.9.0 to 6.9.2. - [Release notes](https://github.com/getsentry/sentry-java/releases) - [Changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-java/compare/6.9.0...6.9.2) --- updated-dependencies: - dependency-name: io.sentry:sentry-android dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index dbb5f5fe05..dd8e6bb11c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -26,7 +26,7 @@ def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.9.0" +def sentry = "6.9.2" def fragment = "1.5.5" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 From eb3117491353478667efd6882edcc86e4e70ad8e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 14:39:22 +0100 Subject: [PATCH 610/679] Toggle selection on long press even when in selection mode --- .../settings/devices/v2/othersessions/OtherSessionsFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 87330b087a..510935e8e6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -341,6 +341,8 @@ class OtherSessionsFragment : override fun onOtherSessionLongClicked(deviceId: String) = withState(viewModel) { state -> if (!state.isSelectModeEnabled) { enableSelectMode(true, deviceId) + } else { + viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(deviceId)) } } From f0dc6e478dd679dd8d29ffe706d39d25559985f0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 15 Dec 2022 16:48:32 +0300 Subject: [PATCH 611/679] Fix ip address visibility in the current session details. --- .../app/features/settings/devices/v2/list/SessionInfoView.kt | 4 ++-- .../features/settings/devices/v2/list/SessionInfoViewState.kt | 2 +- .../settings/devices/v2/overview/SessionOverviewFragment.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index eecec72b0a..5d2daf2941 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -75,7 +75,7 @@ class SessionInfoView @JvmOverloads constructor( renderDeviceLastSeenDetails( sessionInfoViewState.deviceFullInfo.isInactive, sessionInfoViewState.deviceFullInfo.deviceInfo, - sessionInfoViewState.isLastSeenDetailsVisible, + sessionInfoViewState.isLastActivityVisible, sessionInfoViewState.isShowingIpAddress, dateFormatter, drawableProvider, @@ -197,7 +197,7 @@ class SessionInfoView @JvmOverloads constructor( } else { views.sessionInfoLastActivityTextView.isGone = true } - views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress }) + views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isShowingIpAddress }) } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 5d3c4b4f4b..6c7ca809ea 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -24,6 +24,6 @@ data class SessionInfoViewState( val isVerifyButtonVisible: Boolean = true, val isDetailsButtonVisible: Boolean = true, val isLearnMoreLinkVisible: Boolean = false, - val isLastSeenDetailsVisible: Boolean = false, + val isLastActivityVisible: Boolean = false, val isShowingIpAddress: Boolean = false, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index f3df0cced0..399f99201b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -224,7 +224,7 @@ class SessionOverviewFragment : isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted, isDetailsButtonVisible = false, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, - isLastSeenDetailsVisible = !isCurrentSession, + isLastActivityVisible = !isCurrentSession, isShowingIpAddress = viewState.isShowingIpAddress, ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) From bc9ca3fd12f000565c70b86831c3ba7b86e5db40 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 15 Dec 2022 17:26:39 +0300 Subject: [PATCH 612/679] Revert "Fix ip address visibility in the current session details." This reverts commit f0dc6e478dd679dd8d29ffe706d39d25559985f0. --- .../app/features/settings/devices/v2/list/SessionInfoView.kt | 4 ++-- .../features/settings/devices/v2/list/SessionInfoViewState.kt | 2 +- .../settings/devices/v2/overview/SessionOverviewFragment.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 5d2daf2941..eecec72b0a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -75,7 +75,7 @@ class SessionInfoView @JvmOverloads constructor( renderDeviceLastSeenDetails( sessionInfoViewState.deviceFullInfo.isInactive, sessionInfoViewState.deviceFullInfo.deviceInfo, - sessionInfoViewState.isLastActivityVisible, + sessionInfoViewState.isLastSeenDetailsVisible, sessionInfoViewState.isShowingIpAddress, dateFormatter, drawableProvider, @@ -197,7 +197,7 @@ class SessionInfoView @JvmOverloads constructor( } else { views.sessionInfoLastActivityTextView.isGone = true } - views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isShowingIpAddress }) + views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress }) } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 6c7ca809ea..5d3c4b4f4b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -24,6 +24,6 @@ data class SessionInfoViewState( val isVerifyButtonVisible: Boolean = true, val isDetailsButtonVisible: Boolean = true, val isLearnMoreLinkVisible: Boolean = false, - val isLastActivityVisible: Boolean = false, + val isLastSeenDetailsVisible: Boolean = false, val isShowingIpAddress: Boolean = false, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 399f99201b..f3df0cced0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -224,7 +224,7 @@ class SessionOverviewFragment : isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted, isDetailsButtonVisible = false, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, - isLastActivityVisible = !isCurrentSession, + isLastSeenDetailsVisible = !isCurrentSession, isShowingIpAddress = viewState.isShowingIpAddress, ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) From ce23b806987dde5cab2750a18c0958c6bf470866 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 15 Dec 2022 17:29:37 +0300 Subject: [PATCH 613/679] Fix current session ip address visibility. --- .../app/features/settings/devices/v2/list/SessionInfoView.kt | 4 ++-- .../features/settings/devices/v2/list/SessionInfoViewState.kt | 2 +- .../settings/devices/v2/overview/SessionOverviewFragment.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index eecec72b0a..5d2daf2941 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -75,7 +75,7 @@ class SessionInfoView @JvmOverloads constructor( renderDeviceLastSeenDetails( sessionInfoViewState.deviceFullInfo.isInactive, sessionInfoViewState.deviceFullInfo.deviceInfo, - sessionInfoViewState.isLastSeenDetailsVisible, + sessionInfoViewState.isLastActivityVisible, sessionInfoViewState.isShowingIpAddress, dateFormatter, drawableProvider, @@ -197,7 +197,7 @@ class SessionInfoView @JvmOverloads constructor( } else { views.sessionInfoLastActivityTextView.isGone = true } - views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress }) + views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isShowingIpAddress }) } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 5d3c4b4f4b..6c7ca809ea 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -24,6 +24,6 @@ data class SessionInfoViewState( val isVerifyButtonVisible: Boolean = true, val isDetailsButtonVisible: Boolean = true, val isLearnMoreLinkVisible: Boolean = false, - val isLastSeenDetailsVisible: Boolean = false, + val isLastActivityVisible: Boolean = false, val isShowingIpAddress: Boolean = false, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index f3df0cced0..399f99201b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -224,7 +224,7 @@ class SessionOverviewFragment : isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted, isDetailsButtonVisible = false, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, - isLastSeenDetailsVisible = !isCurrentSession, + isLastActivityVisible = !isCurrentSession, isShowingIpAddress = viewState.isShowingIpAddress, ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) From a213338a22d5cbb93b2b3f50ec0a41720444e2dd Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 15 Dec 2022 17:34:35 +0300 Subject: [PATCH 614/679] Add changelog. --- changelog.d/7794.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7794.bugfix diff --git a/changelog.d/7794.bugfix b/changelog.d/7794.bugfix new file mode 100644 index 0000000000..54cd93728b --- /dev/null +++ b/changelog.d/7794.bugfix @@ -0,0 +1 @@ +Fix current session ip address visibility From 319168804475480313612606d16ffb9a6f69e50d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 15:38:50 +0100 Subject: [PATCH 615/679] Adding changelog entry --- changelog.d/7795.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7795.feature diff --git a/changelog.d/7795.feature b/changelog.d/7795.feature new file mode 100644 index 0000000000..50c7e2a8cc --- /dev/null +++ b/changelog.d/7795.feature @@ -0,0 +1 @@ +[Session manager] Security recommendations cards: whole view should be tappable From d7a729740e971b03047fe0c6ff838fcdd6730c76 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 16:02:56 +0100 Subject: [PATCH 616/679] Adding click listener on the whole custom view --- .../settings/devices/v2/list/SecurityRecommendationView.kt | 3 +++ vector/src/main/res/layout/view_security_recommendation.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt index 07202274ad..2a43a9aade 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt @@ -53,6 +53,9 @@ class SecurityRecommendationView @JvmOverloads constructor( setImage(it) } + setOnClickListener { + callback?.onViewAllClicked() + } views.recommendationViewAllButton.setOnClickListener { callback?.onViewAllClicked() } diff --git a/vector/src/main/res/layout/view_security_recommendation.xml b/vector/src/main/res/layout/view_security_recommendation.xml index 6710864048..4a41ca961f 100644 --- a/vector/src/main/res/layout/view_security_recommendation.xml +++ b/vector/src/main/res/layout/view_security_recommendation.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_current_session" + android:foreground="?attr/selectableItemBackground" android:paddingHorizontal="16dp" android:paddingTop="16dp" android:paddingBottom="8dp"> From 301ecdf1f7f11645ae98e772958cf5d9c714b4e9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 15 Dec 2022 16:46:05 +0100 Subject: [PATCH 617/679] Adding changelog entry --- changelog.d/7797.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7797.feature diff --git a/changelog.d/7797.feature b/changelog.d/7797.feature new file mode 100644 index 0000000000..280f310b91 --- /dev/null +++ b/changelog.d/7797.feature @@ -0,0 +1 @@ +[Session manager] Other sessions list: Security recommendation header should not be sticky From c2d25c8564e3eeb4637138c213c74c2b90610c18 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 16 Dec 2022 09:46:52 +0100 Subject: [PATCH 618/679] Collapsing header in other sessions screen --- .../res/layout/fragment_other_sessions.xml | 208 +++++++++--------- 1 file changed, 110 insertions(+), 98 deletions(-) diff --git a/vector/src/main/res/layout/fragment_other_sessions.xml b/vector/src/main/res/layout/fragment_other_sessions.xml index e25b8b185f..62384b7ee1 100644 --- a/vector/src/main/res/layout/fragment_other_sessions.xml +++ b/vector/src/main/res/layout/fragment_other_sessions.xml @@ -1,120 +1,132 @@ - - + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + android:layout_marginStart="72dp" + android:layout_marginTop="32dp" + android:layout_marginEnd="16dp" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + - + android:layout_marginTop="20dp" + android:gravity="start" + android:padding="0dp" + android:text="@string/device_manager_other_sessions_clear_filter" /> - + + + + + + + + + + + + + + + + + android:layout_gravity="end" + android:layout_marginEnd="8dp" + android:padding="8dp"> - + - + - + - + + - - - - - - - + -