Render incoming Element Call in the timeline (unsupported)

This commit is contained in:
Benoit Marty 2024-11-12 14:06:11 +01:00
parent ac94bff81e
commit 066545a4b3
8 changed files with 290 additions and 4 deletions

View file

@ -2937,6 +2937,8 @@
<string name="call_slide_to_end_conference">Slide to end the call</string> <string name="call_slide_to_end_conference">Slide to end the call</string>
<string name="call_unsupported_matrix_rtc_call">Unsupported call. The new Element X app is needed to join this call.</string>
<string name="re_authentication_activity_title">Re-Authentication Needed</string> <string name="re_authentication_activity_title">Re-Authentication Needed</string>
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name --> <!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->
<string name="re_authentication_default_confirm_text">${app_name} requires you to enter your credentials to perform this action.</string> <string name="re_authentication_default_confirm_text">${app_name} requires you to enter your credentials to perform this action.</string>

View file

@ -87,6 +87,9 @@ object EventType {
// This type is not processed by the client, just sent to the server // This type is not processed by the client, just sent to the server
const val CALL_REPLACES = "m.call.replaces" const val CALL_REPLACES = "m.call.replaces"
// Element Call
val ELEMENT_CALL_NOTIFY = StableUnstableId(stable = "m.call.notify", unstable = "org.matrix.msc4075.call.notify")
// Key share events // Key share events
const val ROOM_KEY_REQUEST = "m.room_key_request" const val ROOM_KEY_REQUEST = "m.room_key_request"
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 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.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ElementCallNotifyContent(
@Json(name = "application") val application: String? = null,
@Json(name = "call_id") val callId: String? = null,
@Json(name = "m.mentions") val mentions: Mentions? = null,
@Json(name = "notify_type") val notifyType: String? = null,
)
@JsonClass(generateAdapter = true)
data class Mentions(
@Json(name = "room") val room: Boolean? = null,
@Json(name = "user_ids") val userIds: List<String>? = null,
)

View file

@ -0,0 +1,101 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
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.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.app.features.home.room.detail.timeline.item.ElementCallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.ElementCallTileTimelineItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
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.toModel
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.ElementCallNotifyContent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class ElementCallItemFactory @Inject constructor(
private val session: Session,
private val userPreferencesProvider: UserPreferencesProvider,
private val messageColorProvider: MessageColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val noticeItemFactory: NoticeItemFactory
) {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
if (event.root.eventId == null) return null
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
val roomSummary = params.partialState.roomSummary ?: return null
val informationData = messageInformationDataFactory.create(params)
val callItem = when (event.root.getClearType()) {
in EventType.ELEMENT_CALL_NOTIFY.values -> {
val notifyContent: ElementCallNotifyContent = event.root.content.toModel() ?: return null
createElementCallTileTimelineItem(
roomSummary = roomSummary,
callId = notifyContent.callId.orEmpty(),
callStatus = ElementCallTileTimelineItem.CallStatus.INVITED,
callKind = ElementCallTileTimelineItem.CallKind.VIDEO,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
reactionsSummaryEvents = params.reactionsSummaryEvents
)
}
else -> null
}
return if (callItem == null && showHiddenEvents) {
// Fallback to notice item for showing hidden events
noticeItemFactory.create(params)
} else {
callItem
}
}
private fun createElementCallTileTimelineItem(
roomSummary: RoomSummary,
callId: String,
callKind: ElementCallTileTimelineItem.CallKind,
callStatus: ElementCallTileTimelineItem.CallStatus,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
reactionsSummaryEvents: ReactionsSummaryEvents?
): ElementCallTileTimelineItem? {
val userOfInterest = roomSummary.toMatrixItem()
val attributes = messageItemAttributesFactory.create(null, informationData, callback, reactionsSummaryEvents).let {
ElementCallTileTimelineItem.Attributes(
callId = callId,
callKind = callKind,
callStatus = callStatus,
informationData = informationData,
avatarRenderer = it.avatarRenderer,
messageColorProvider = messageColorProvider,
itemClickListener = it.itemClickListener,
itemLongClickListener = it.itemLongClickListener,
reactionPillCallback = it.reactionPillCallback,
readReceiptsCallback = it.readReceiptsCallback,
userOfInterest = userOfInterest,
callback = callback,
reactionsSummaryEvents = reactionsSummaryEvents
)
}
return ElementCallTileTimelineItem_()
.attributes(attributes)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
}

View file

@ -32,6 +32,7 @@ class TimelineItemFactory @Inject constructor(
private val widgetItemFactory: WidgetItemFactory, private val widgetItemFactory: WidgetItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory, private val verificationConclusionItemFactory: VerificationItemFactory,
private val callItemFactory: CallItemFactory, private val callItemFactory: CallItemFactory,
private val elementCallItemFactory: ElementCallItemFactory,
private val decryptionFailureTracker: DecryptionFailureTracker, private val decryptionFailureTracker: DecryptionFailureTracker,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val session: Session, private val session: Session,
@ -119,6 +120,8 @@ class TimelineItemFactory @Inject constructor(
noticeItemFactory.create(params) noticeItemFactory.create(params)
} }
} }
// Element Call
in EventType.ELEMENT_CALL_NOTIFY.values -> elementCallItemFactory.create(params)
// Calls // Calls
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,

View file

@ -0,0 +1,88 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package im.vector.app.features.home.room.detail.timeline.item
import android.content.res.Resources
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.lib.strings.CommonStrings
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass
abstract class ElementCallTileTimelineItem : AbsBaseMessageItem<ElementCallTileTimelineItem.Holder>(R.layout.item_timeline_event_base_state) {
override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes
override fun isCacheable() = false
@EpoxyAttribute
lateinit var attributes: Attributes
override fun getViewStubId() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
holder.creatorNameView.text = attributes.userOfInterest.getBestName()
attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView)
renderSendState(holder.view, null, holder.failedToSendIndicator)
}
class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
val creatorAvatarView by bind<ImageView>(R.id.itemCallCreatorAvatar)
val creatorNameView by bind<TextView>(R.id.itemCallCreatorNameTextView)
val endGuideline by bind<View>(R.id.messageEndGuideline)
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
val resources: Resources
get() = view.context.resources
}
companion object {
private val STUB_ID = R.id.messageElementCallStub
}
data class Attributes(
val callId: String,
val callKind: CallKind,
val callStatus: CallStatus,
val userOfInterest: MatrixItem,
val callback: TimelineEventController.Callback? = null,
override val informationData: MessageInformationData,
override val avatarRenderer: AvatarRenderer,
override val messageColorProvider: MessageColorProvider,
override val itemLongClickListener: View.OnLongClickListener? = null,
override val itemClickListener: ClickListener? = null,
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val reactionsSummaryEvents: ReactionsSummaryEvents? = null
) : AbsBaseMessageItem.Attributes
enum class CallKind(@DrawableRes val icon: Int, @StringRes val title: Int) {
VIDEO(R.drawable.ic_call_video_small, CommonStrings.action_video_call),
}
enum class CallStatus {
INVITED,
}
}

View file

@ -52,21 +52,28 @@
android:id="@+id/messageVerificationRequestStub" android:id="@+id/messageVerificationRequestStub"
style="@style/TimelineContentStubBaseParams" style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_verification_stub" android:layout="@layout/item_timeline_event_verification_stub"
tools:layout_marginTop="250dp" tools:layout_marginTop="200dp"
tools:visibility="visible" /> tools:visibility="visible" />
<ViewStub <ViewStub
android:id="@+id/messageVerificationDoneStub" android:id="@+id/messageVerificationDoneStub"
style="@style/TimelineContentStubBaseParams" style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_status_tile_stub" android:layout="@layout/item_timeline_event_status_tile_stub"
tools:layout_marginTop="450dp" tools:layout_marginTop="360dp"
tools:visibility="visible" /> tools:visibility="visible" />
<ViewStub <ViewStub
android:id="@+id/messageWidgetStub" android:id="@+id/messageWidgetStub"
style="@style/TimelineContentStubBaseParams" style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_widget_stub" android:layout="@layout/item_timeline_event_widget_stub"
tools:layout_marginTop="280dp" tools:layout_marginTop="440dp"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageElementCallStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_element_call_tile_stub"
tools:layout_marginTop="520dp"
tools:visibility="visible" /> tools:visibility="visible" />
</FrameLayout> </FrameLayout>
@ -122,4 +129,4 @@
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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:orientation="vertical"
tools:viewBindingIgnore="true">
<ImageView
android:id="@+id/itemCallCreatorAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:importantForAccessibility="no"
tools:src="@sample/user_round_avatars" />
<TextView
android:id="@+id/itemCallCreatorNameTextView"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:drawablePadding="6dp"
android:gravity="center"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
tools:text="@sample/users.json/data/displayName" />
<TextView
android:id="@+id/itemCallStatusTextView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:drawablePadding="8dp"
android:gravity="start"
android:maxLines="3"
android:text="@string/call_unsupported_matrix_rtc_call"
android:textColor="?vctr_content_secondary"
app:drawableStartCompat="@drawable/ic_call_audio_small" />
</LinearLayout>