Skip to content

Commit

Permalink
Merge pull request #8941 from element-hq/feature/bma/elementCall
Browse files Browse the repository at this point in the history
Element call incoming call
  • Loading branch information
bmarty authored Nov 12, 2024
2 parents 7051c0c + 12adddb commit ea170fc
Show file tree
Hide file tree
Showing 15 changed files with 319 additions and 7 deletions.
1 change: 1 addition & 0 deletions changelog.d/8938.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Indicate when calls are unsupported in the timeline/notifications
3 changes: 3 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2939,6 +2939,9 @@

<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>
<!-- 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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,11 @@ fun Event.getPollContent(): MessagePollContent? {
}

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() =
this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ object EventType {
// This type is not processed by the client, just sent to the server
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
const val ROOM_KEY_REQUEST = "m.room_key_request"
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
in EventType.POLL_START.values,
in EventType.POLL_END.values,
in EventType.STATE_ROOM_BEACON_INFO.values,
in EventType.ELEMENT_CALL_NOTIFY.values,
EventType.MESSAGE,
EventType.REDACTION,
EventType.ENCRYPTED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ class MessageActionsViewModel @AssistedInject constructor(
in EventType.POLL_END.values -> {
stringProvider.getString(CommonStrings.message_reply_to_ended_poll_preview)
}
in EventType.ELEMENT_CALL_NOTIFY.values -> {
stringProvider.getString(CommonStrings.call_unsupported)
}
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class TimelineItemFactory @Inject constructor(
private val widgetItemFactory: WidgetItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory,
private val callItemFactory: CallItemFactory,
private val elementCallItemFactory: ElementCallItemFactory,
private val decryptionFailureTracker: DecryptionFailureTracker,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val session: Session,
Expand Down Expand Up @@ -119,6 +120,8 @@ class TimelineItemFactory @Inject constructor(
noticeItemFactory.create(params)
}
}
// Element Call
in EventType.ELEMENT_CALL_NOTIFY.values -> elementCallItemFactory.create(params)
// Calls
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ class DisplayableEventFormatter @Inject constructor(
in EventType.STATE_ROOM_BEACON_INFO.values -> {
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 -> {
formatVoiceBroadcastEvent(timelineEvent.root, isDm, senderName)
}
Expand Down Expand Up @@ -243,6 +246,9 @@ class DisplayableEventFormatter @Inject constructor(
in EventType.STATE_ROOM_BEACON_INFO.values -> {
stringProvider.getString(CommonStrings.sent_live_location)
}
in EventType.ELEMENT_CALL_NOTIFY.values -> {
stringProvider.getString(CommonStrings.call_unsupported)
}
else -> {
span {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ object TimelineDisplayableEvents {
) +
EventType.POLL_START.values +
EventType.POLL_END.values +
EventType.ELEMENT_CALL_NOTIFY.values +
EventType.STATE_ROOM_BEACON_INFO.values +
EventType.BEACON_LOCATION_DATA.values
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.model.Membership
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.isUserMentioned
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.getEditedEventId
Expand Down Expand Up @@ -149,9 +151,11 @@ class NotifiableEventResolver @Inject constructor(
)
} else {
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 {
event.root.supportsNotification() -> {
isIncomingElementCall || event.root.supportsNotification() -> {
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
Expand Down
15 changes: 11 additions & 4 deletions vector/src/main/res/layout/item_timeline_event_base_state.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,28 @@
android:id="@+id/messageVerificationRequestStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_verification_stub"
tools:layout_marginTop="250dp"
tools:layout_marginTop="200dp"
tools:visibility="visible" />

<ViewStub
android:id="@+id/messageVerificationDoneStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_status_tile_stub"
tools:layout_marginTop="450dp"
tools:layout_marginTop="360dp"
tools:visibility="visible" />

<ViewStub
android:id="@+id/messageWidgetStub"
style="@style/TimelineContentStubBaseParams"
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" />

</FrameLayout>
Expand Down Expand Up @@ -122,4 +129,4 @@

</LinearLayout>

</RelativeLayout>
</RelativeLayout>
Loading

0 comments on commit ea170fc

Please sign in to comment.