diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 63a3fad109..c7a477cd42 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===83 +enum class===85 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index 44b85df93a..b7f97dc6f7 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -18,11 +18,19 @@ package im.vector.app.core.extensions import android.text.Spannable import android.text.SpannableString +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.UnderlineSpan +import android.view.View import android.widget.TextView import androidx.annotation.AttrRes +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible import com.google.android.material.snackbar.Snackbar import im.vector.app.R @@ -48,11 +56,13 @@ fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true * @param coloredTextRes the resource id of the colored part of the text * @param colorAttribute attribute of the color. Default to colorAccent * @param underline true to also underline the text. Default to false + * @param onClick attributes to handle click on the colored part if needed */ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, @StringRes coloredTextRes: Int, @AttrRes colorAttribute: Int = R.attr.colorAccent, - underline: Boolean = false) { + underline: Boolean = false, + onClick: (() -> Unit)?) { val coloredPart = resources.getString(coloredTextRes) // Insert colored part into the full text val fullText = resources.getString(fullTextRes, coloredPart) @@ -65,12 +75,38 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, text = SpannableString(fullText) .apply { setSpan(foregroundSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + if (onClick != null) { + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + onClick() + } + + override fun updateDrawState(ds: TextPaint) { + ds.color = color + ds.isUnderlineText = !underline + } + } + setSpan(clickableSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + movementMethod = LinkMovementMethod.getInstance() + } if (underline) { setSpan(UnderlineSpan(), index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } } +fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @ColorRes tintColor: Int? = null) { + val icon = if (tintColor != null) { + val tint = ContextCompat.getColor(context, tintColor) + ContextCompat.getDrawable(context, iconRes)?.also { + DrawableCompat.setTint(it.mutate(), tint) + } + } else { + ContextCompat.getDrawable(context, iconRes) + } + setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) +} + /** * Set long click listener to copy the current text of the TextView to the clipboard and show a Snackbar */ diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index c6a5af5843..355bd64380 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -339,12 +339,12 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis const val INCOMING_RINGING = "INCOMING_RINGING" const val INCOMING_ACCEPT = "INCOMING_ACCEPT" - fun newIntent(context: Context, mxCall: MxCallDetail): Intent { + fun newIntent(context: Context, mxCall: MxCallDetail, mode: String?): Intent { return Intent(context, VectorCallActivity::class.java).apply { // what could be the best flags? flags = Intent.FLAG_ACTIVITY_NEW_TASK putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.opponentUserId, !mxCall.isOutgoing, mxCall.isVideoCall)) - putExtra(EXTRA_MODE, OUTGOING_CREATED) + putExtra(EXTRA_MODE, mode) } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 8e94604260..f3665f0b45 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -200,7 +200,7 @@ class WebRtcCallManager @Inject constructor( callId = mxCall.callId) // start the activity now - context.startActivity(VectorCallActivity.newIntent(context, mxCall)) + context.startActivity(VectorCallActivity.newIntent(context, mxCall, VectorCallActivity.OUTGOING_CREATED)) } override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { 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 8891218a11..518da4adb0 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 @@ -73,6 +73,7 @@ sealed class RoomDetailAction : VectorViewModelAction { object ResendAll : RoomDetailAction() data class StartCall(val isVideo: Boolean) : RoomDetailAction() + data class AcceptCall(val callId: String): RoomDetailAction() object EndCall : RoomDetailAction() data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 8dc85bd8af..f1617b92fb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -368,6 +368,7 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) + is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) @@ -389,6 +390,15 @@ class RoomDetailFragment @Inject constructor( } } + private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) { + val intent = VectorCallActivity.newIntent( + context = vectorBaseActivity, + mxCall = event.call.mxCall, + mode = VectorCallActivity.INCOMING_ACCEPT + ) + startActivity(intent) + } + override fun onImageReady(uri: Uri?) { uri ?: return roomDetailViewModel.handle( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index d5d94a0ca5..a22f315082 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import androidx.annotation.StringRes import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.command.Command import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem @@ -73,6 +74,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents { abstract class SendMessageResult : RoomDetailViewEvents() + data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents() + object DisplayPromptForIntegrationManager : RoomDetailViewEvents() object DisplayEnableIntegrationsWarning : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 27cd1b24ae..9221fc8041 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -269,6 +269,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.AcceptCall -> handleAcceptCall(action) is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) @@ -289,6 +290,12 @@ class RoomDetailViewModel @AssistedInject constructor( }.exhaustive } + private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) { + callManager.getCallById(action.callId)?.also { + _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) + } + } + private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { viewModelScope.launch(Dispatchers.IO) { try { 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 bddc7fa126..d6061cab12 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 @@ -30,10 +30,12 @@ import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory +import im.vector.app.features.home.room.detail.timeline.factory.NoticeItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @@ -43,6 +45,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisi import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData @@ -72,6 +75,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val session: Session, + private val callManager: WebRtcCallManager, + private val noticeItemFactory: NoticeItemFactory, @TimelineEventControllerHandler private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { @@ -184,10 +189,27 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun intercept(models: MutableList>) = synchronized(modelCache) { positionOfReadMarker = null adapterPositionMapping.clear() - models.forEachIndexed { index, epoxyModel -> + val callIds = mutableSetOf() + val modelsIterator = models.listIterator() + val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents() + modelsIterator.withIndex().forEach { + val index = it.index + val epoxyModel = it.value + if (epoxyModel is CallTileTimelineItem) { + val callId = epoxyModel.attributes.callId + val call = callManager.getCallById(callId) + // We should remove the call tile if we already have one for this call or + // if this is an active call tile without an actual call (which can happen with permalink) + val shouldRemoveCallItem = callIds.contains(callId) || (call == null && epoxyModel.attributes.callStatus.isActive()) + if (shouldRemoveCallItem && !showHiddenEvents) { + modelsIterator.remove() + return@forEach + } + callIds.add(callId) + } if (epoxyModel is BaseEventItem) { - epoxyModel.getEventIds().forEach { - adapterPositionMapping[it] = index + epoxyModel.getEventIds().forEach { eventId -> + adapterPositionMapping[eventId] = index } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt new file mode 100644 index 0000000000..8c972a8684 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2019 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.factory + +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.call.webrtc.WebRtcCallManager +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.helper.RoomSummaryHolder +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_ +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +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.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class CallItemFactory @Inject constructor( + private val messageColorProvider: MessageColorProvider, + private val messageInformationDataFactory: MessageInformationDataFactory, + private val messageItemAttributesFactory: MessageItemAttributesFactory, + private val avatarSizeProvider: AvatarSizeProvider, + private val roomSummaryHolder: RoomSummaryHolder, + private val callManager: WebRtcCallManager +) { + + fun create(event: TimelineEvent, + highlight: Boolean, + callback: TimelineEventController.Callback? + ): VectorEpoxyModel<*>? { + if (event.root.eventId == null) return null + val informationData = messageInformationDataFactory.create(event, null) + val callSignalingContent = event.getCallSignallingContent() ?: return null + val callId = callSignalingContent.callId ?: return null + val call = callManager.getCallById(callId) + val callKind = if (call?.mxCall?.isVideoCall.orFalse()) { + CallTileTimelineItem.CallKind.VIDEO + } else { + CallTileTimelineItem.CallKind.AUDIO + } + return when (event.root.getClearType()) { + EventType.CALL_ANSWER -> { + createCallTileTimelineItem( + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.IN_CALL, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = call != null + ) + } + EventType.CALL_INVITE -> { + createCallTileTimelineItem( + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.INVITED, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = call != null + ) + } + EventType.CALL_REJECT -> { + createCallTileTimelineItem( + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.REJECTED, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = false + ) + } + EventType.CALL_HANGUP -> { + createCallTileTimelineItem( + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.ENDED, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = false + ) + } + else -> null + } + } + + private fun TimelineEvent.getCallSignallingContent(): CallSignallingContent? { + return when (root.getClearType()) { + EventType.CALL_INVITE -> root.getClearContent().toModel() + EventType.CALL_HANGUP -> root.getClearContent().toModel() + EventType.CALL_REJECT -> root.getClearContent().toModel() + EventType.CALL_ANSWER -> root.getClearContent().toModel() + else -> null + } + } + + private fun createCallTileTimelineItem( + callId: String, + callKind: CallTileTimelineItem.CallKind, + callStatus: CallTileTimelineItem.CallStatus, + informationData: MessageInformationData, + highlight: Boolean, + isStillActive: Boolean, + callback: TimelineEventController.Callback? + ): CallTileTimelineItem? { + val userOfInterest = roomSummaryHolder.roomSummary?.toMatrixItem() ?: return null + val attributes = messageItemAttributesFactory.create(null, informationData, callback).let { + CallTileTimelineItem.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, + isStillActive = isStillActive + ) + } + return CallTileTimelineItem_() + .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 243cbbd0e6..432736dba0 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 @@ -34,6 +34,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val roomCreateItemFactory: RoomCreateItemFactory, private val roomSummaryHolder: RoomSummaryHolder, private val verificationConclusionItemFactory: VerificationItemFactory, + private val callItemFactory: CallItemFactory, private val userPreferencesProvider: UserPreferencesProvider) { fun create(event: TimelineEvent, @@ -60,15 +61,17 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET, - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER, EventType.STATE_ROOM_POWER_LEVELS, EventType.REACTION, EventType.REDACTION -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) + // Calls + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_REJECT, + EventType.CALL_ANSWER -> callItemFactory.create(event, highlight, callback) // Crypto EventType.ENCRYPTED -> { if (event.root.isRedacted()) { 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 4fcac6c7f7..eb5b8081f9 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 @@ -38,6 +38,7 @@ object TimelineDisplayableEvents { EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, + EventType.CALL_REJECT, EventType.ENCRYPTED, EventType.STATE_ROOM_ENCRYPTION, EventType.STATE_ROOM_GUEST_ACCESS, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt new file mode 100644 index 0000000000..baf4a2b5cd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2020 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.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.view.isVisible +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.extensions.setLeftDrawable +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.RoomDetailAction +import im.vector.app.features.home.room.detail.timeline.MessageColorProvider +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) +abstract class CallTileTimelineItem : AbsBaseMessageItem() { + + override val baseAttributes: AbsBaseMessageItem.Attributes + get() = attributes + + @EpoxyAttribute + lateinit var attributes: Attributes + + override fun getViewType() = 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) + holder.callKindView.setText(attributes.callKind.title) + holder.callKindView.setLeftDrawable(attributes.callKind.icon) + if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) { + holder.acceptRejectViewGroup.isVisible = true + holder.acceptView.setOnClickListener { + attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId)) + } + holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.color.riotx_notice) + holder.rejectView.setOnClickListener { + attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall) + } + holder.statusView.isVisible = false + when (attributes.callKind) { + CallKind.CONFERENCE -> { + holder.rejectView.setText(R.string.ignore) + holder.acceptView.setText(R.string.join) + holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.color.riotx_accent) + } + CallKind.AUDIO -> { + holder.rejectView.setText(R.string.call_notification_reject) + holder.acceptView.setText(R.string.call_notification_answer) + holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.color.riotx_accent) + } + CallKind.VIDEO -> { + holder.rejectView.setText(R.string.call_notification_reject) + holder.acceptView.setText(R.string.call_notification_answer) + holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.color.riotx_accent) + } + } + } else { + holder.acceptRejectViewGroup.isVisible = false + holder.statusView.isVisible = true + } + holder.statusView.setCallStatus(attributes) + renderSendState(holder.view, null, holder.failedToSendIndicator) + } + + private fun TextView.setCallStatus(attributes: Attributes) { + when (attributes.callStatus) { + CallStatus.INVITED -> if (attributes.informationData.sentByMe) { + setText(R.string.call_tile_you_started_call) + } else { + text = context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName()) + } + CallStatus.IN_CALL -> setText(R.string.call_tile_in_call) + CallStatus.REJECTED -> if (attributes.informationData.sentByMe) { + setTextWithColoredPart(R.string.call_tile_you_declined, R.string.call_tile_call_back) { + val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO) + attributes.callback?.onTimelineItemAction(callbackAction) + } + } else { + text = context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName()) + } + CallStatus.ENDED -> setText(R.string.call_tile_ended) + } + } + + class Holder : AbsBaseMessageItem.Holder(STUB_ID) { + val acceptView by bind