Merge pull request #8941 from element-hq/feature/bma/elementCall

Element call incoming call
This commit is contained in:
Benoit Marty 2024-11-12 16:40:12 +01:00 committed by GitHub
commit ea170fc2af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 319 additions and 7 deletions

1
changelog.d/8938.misc Normal file
View file

@ -0,0 +1 @@
Indicate when calls are unsupported in the timeline/notifications

View file

@ -2939,6 +2939,9 @@
<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">Unsupported 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

@ -499,7 +499,11 @@ fun Event.getPollContent(): MessagePollContent? {
} }
fun Event.supportsNotification() = fun Event.supportsNotification() =
this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.POLL_END.values + EventType.STATE_ROOM_BEACON_INFO.values this.getClearType() in EventType.MESSAGE +
EventType.POLL_START.values +
EventType.POLL_END.values +
EventType.STATE_ROOM_BEACON_INFO.values +
EventType.ELEMENT_CALL_NOTIFY.values
fun Event.isContentReportable() = fun Event.isContentReportable() =
this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values

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,39 @@
/*
* 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,
)
fun ElementCallNotifyContent.isUserMentioned(userId: String): Boolean {
return mentions?.room == true ||
mentions?.userIds?.contains(userId) == true
}

View file

@ -61,6 +61,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
in EventType.POLL_START.values, in EventType.POLL_START.values,
in EventType.POLL_END.values, in EventType.POLL_END.values,
in EventType.STATE_ROOM_BEACON_INFO.values, in EventType.STATE_ROOM_BEACON_INFO.values,
in EventType.ELEMENT_CALL_NOTIFY.values,
EventType.MESSAGE, EventType.MESSAGE,
EventType.REDACTION, EventType.REDACTION,
EventType.ENCRYPTED, EventType.ENCRYPTED,

View file

@ -214,6 +214,9 @@ class MessageActionsViewModel @AssistedInject constructor(
in EventType.POLL_END.values -> { in EventType.POLL_END.values -> {
stringProvider.getString(CommonStrings.message_reply_to_ended_poll_preview) stringProvider.getString(CommonStrings.message_reply_to_ended_poll_preview)
} }
in EventType.ELEMENT_CALL_NOTIFY.values -> {
stringProvider.getString(CommonStrings.call_unsupported)
}
else -> null else -> 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

@ -142,6 +142,9 @@ class DisplayableEventFormatter @Inject constructor(
in EventType.STATE_ROOM_BEACON_INFO.values -> { in EventType.STATE_ROOM_BEACON_INFO.values -> {
simpleFormat(senderName, stringProvider.getString(CommonStrings.sent_live_location), appendAuthor) simpleFormat(senderName, stringProvider.getString(CommonStrings.sent_live_location), appendAuthor)
} }
in EventType.ELEMENT_CALL_NOTIFY.values -> {
simpleFormat(senderName, stringProvider.getString(CommonStrings.call_unsupported), appendAuthor)
}
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> { VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> {
formatVoiceBroadcastEvent(timelineEvent.root, isDm, senderName) formatVoiceBroadcastEvent(timelineEvent.root, isDm, senderName)
} }
@ -243,6 +246,9 @@ class DisplayableEventFormatter @Inject constructor(
in EventType.STATE_ROOM_BEACON_INFO.values -> { in EventType.STATE_ROOM_BEACON_INFO.values -> {
stringProvider.getString(CommonStrings.sent_live_location) stringProvider.getString(CommonStrings.sent_live_location)
} }
in EventType.ELEMENT_CALL_NOTIFY.values -> {
stringProvider.getString(CommonStrings.call_unsupported)
}
else -> { else -> {
span { span {
} }

View file

@ -47,6 +47,7 @@ object TimelineDisplayableEvents {
) + ) +
EventType.POLL_START.values + EventType.POLL_START.values +
EventType.POLL_END.values + EventType.POLL_END.values +
EventType.ELEMENT_CALL_NOTIFY.values +
EventType.STATE_ROOM_BEACON_INFO.values + EventType.STATE_ROOM_BEACON_INFO.values +
EventType.BEACON_LOCATION_DATA.values EventType.BEACON_LOCATION_DATA.values
} }

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

@ -34,7 +34,9 @@ import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.Membership 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.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.ElementCallNotifyContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.isUserMentioned
import org.matrix.android.sdk.api.session.room.sender.SenderInfo 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.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId
@ -149,9 +151,11 @@ class NotifiableEventResolver @Inject constructor(
) )
} else { } else {
event.attemptToDecryptIfNeeded(session) event.attemptToDecryptIfNeeded(session)
// only convert encrypted messages to NotifiableMessageEvents // For incoming Element Call, check that the user is mentioned
val isIncomingElementCall = event.root.getClearType() in EventType.ELEMENT_CALL_NOTIFY.values &&
event.root.getClearContent()?.toModel<ElementCallNotifyContent>()?.isUserMentioned(session.myUserId) == true
when { when {
event.root.supportsNotification() -> { isIncomingElementCall || event.root.supportsNotification() -> {
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: "" val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName val senderDisplayName = event.senderInfo.disambiguatedDisplayName

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>

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>