diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 30e6ef673e..f7f1b8488f 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2937,6 +2937,8 @@ Slide to end the call + Unsupported call. The new Element X app is needed to join this call. + Re-Authentication Needed ${app_name} requires you to enter your credentials to perform this action. 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 9228f76db2..78bbc7cd6b 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 @@ -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" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ElementCallNotifyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ElementCallNotifyContent.kt new file mode 100644 index 0000000000..50bef28fdc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ElementCallNotifyContent.kt @@ -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? = null, +) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ElementCallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ElementCallItemFactory.kt new file mode 100644 index 0000000000..2c0b49eeab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ElementCallItemFactory.kt @@ -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) + } +} 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 ce3694c637..51a08b07f6 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 @@ -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, @@ -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, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ElementCallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ElementCallTileTimelineItem.kt new file mode 100644 index 0000000000..33d5e33661 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ElementCallTileTimelineItem.kt @@ -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(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 { + 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(R.id.itemCallCreatorAvatar) + val creatorNameView by bind(R.id.itemCallCreatorNameTextView) + val endGuideline by bind(R.id.messageEndGuideline) + val failedToSendIndicator by bind(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, + } +} diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml index 927abbfd16..f94cb8801b 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_state.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml @@ -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" /> + + @@ -122,4 +129,4 @@ - \ No newline at end of file + diff --git a/vector/src/main/res/layout/item_timeline_event_element_call_tile_stub.xml b/vector/src/main/res/layout/item_timeline_event_element_call_tile_stub.xml new file mode 100644 index 0000000000..5f0a924ff9 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_element_call_tile_stub.xml @@ -0,0 +1,48 @@ + + + + + + + + + +