Merge pull request #2517 from vector-im/feature/fga/voip_timeline
Feature/fga/voip timeline
This commit is contained in:
commit
44cc62622d
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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<EpoxyModel<*>>) = synchronized(modelCache) {
|
||||
positionOfReadMarker = null
|
||||
adapterPositionMapping.clear()
|
||||
models.forEachIndexed { index, epoxyModel ->
|
||||
val callIds = mutableSetOf<String>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<CallInviteContent>()
|
||||
EventType.CALL_HANGUP -> root.getClearContent().toModel<CallHangupContent>()
|
||||
EventType.CALL_REJECT -> root.getClearContent().toModel<CallRejectContent>()
|
||||
EventType.CALL_ANSWER -> root.getClearContent().toModel<CallAnswerContent>()
|
||||
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)
|
||||
}
|
||||
}
|
@ -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()) {
|
||||
|
@ -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,
|
||||
|
@ -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<CallTileTimelineItem.Holder>() {
|
||||
|
||||
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<RelativeLayout.LayoutParams> {
|
||||
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<Button>(R.id.itemCallAcceptView)
|
||||
val rejectView by bind<Button>(R.id.itemCallRejectView)
|
||||
val acceptRejectViewGroup by bind<ViewGroup>(R.id.itemCallAcceptRejectViewGroup)
|
||||
val callKindView by bind<TextView>(R.id.itemCallKindTextView)
|
||||
val creatorAvatarView by bind<ImageView>(R.id.itemCallCreatorAvatar)
|
||||
val creatorNameView by bind<TextView>(R.id.itemCallCreatorNameTextView)
|
||||
val statusView by bind<TextView>(R.id.itemCallStatusTextView)
|
||||
val endGuideline by bind<View>(R.id.messageEndGuideline)
|
||||
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageCallStub
|
||||
}
|
||||
|
||||
data class Attributes(
|
||||
val callId: String,
|
||||
val callKind: CallKind,
|
||||
val callStatus: CallStatus,
|
||||
val userOfInterest: MatrixItem,
|
||||
val isStillActive: Boolean,
|
||||
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: View.OnClickListener? = null,
|
||||
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
|
||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
|
||||
) : AbsBaseMessageItem.Attributes
|
||||
|
||||
enum class CallKind(@DrawableRes val icon: Int, @StringRes val title: Int) {
|
||||
VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call),
|
||||
AUDIO(R.drawable.ic_call_audio_small, R.string.action_voice_call),
|
||||
CONFERENCE(R.drawable.ic_call_conference_small, R.string.conference_call_in_progress)
|
||||
}
|
||||
|
||||
enum class CallStatus {
|
||||
INVITED,
|
||||
IN_CALL,
|
||||
REJECTED,
|
||||
ENDED;
|
||||
|
||||
fun isActive() = this == INVITED || this == IN_CALL
|
||||
}
|
||||
}
|
9
vector/src/main/res/drawable/ic_call_audio_small.xml
Normal file
9
vector/src/main/res/drawable/ic_call_audio_small.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="14dp"
|
||||
android:height="14dp"
|
||||
android:viewportWidth="14"
|
||||
android:viewportHeight="14">
|
||||
<path
|
||||
android:pathData="M4.3514,9.6408C5.1121,10.4621 6.9433,11.8842 7.4424,12.176C7.4719,12.1933 7.5057,12.2134 7.5435,12.2358C8.3051,12.6886 10.6916,14.1072 12.4304,12.7796C13.7775,11.751 13.3395,10.5939 12.886,10.25C12.5756,10.0085 11.661,9.3429 10.8005,8.7434C9.9555,8.1548 9.4846,8.6264 9.1662,8.9453C9.1603,8.9512 9.1545,8.957 9.1488,8.9627L8.5082,9.6034C8.345,9.7665 8.0968,9.707 7.8591,9.5203C7.0062,8.8707 6.3788,8.2439 6.0649,7.93L6.0623,7.9273C5.7484,7.6135 5.1293,6.9938 4.4798,6.1409C4.2931,5.9032 4.2335,5.655 4.3967,5.4919L5.0373,4.8512C5.0431,4.8455 5.0489,4.8397 5.0547,4.8338C5.3736,4.5154 5.8453,4.0445 5.2566,3.1995C4.6571,2.339 3.9915,1.4244 3.7501,1.114C3.4061,0.6606 2.249,0.2226 1.2205,1.5697C-0.1072,3.3084 1.3115,5.6949 1.7642,6.4565C1.7867,6.4943 1.8068,6.5281 1.824,6.5576C2.1159,7.0567 3.5301,8.8801 4.3514,9.6408Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
14
vector/src/main/res/drawable/ic_call_conference_small.xml
Normal file
14
vector/src/main/res/drawable/ic_call_conference_small.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16ZM5.3333,8.3333C6.4379,8.3333 7.3333,7.3633 7.3333,6.1667C7.3333,4.97 6.4379,4 5.3333,4C4.2288,4 3.3333,4.97 3.3333,6.1667C3.3333,7.3633 4.2288,8.3333 5.3333,8.3333ZM11.5043,9.1296C12.472,9.1296 13.2564,8.2798 13.2564,7.2315C13.2564,6.1832 12.472,5.3333 11.5043,5.3333C10.5366,5.3333 9.7522,6.1832 9.7522,7.2315C9.7522,8.2798 10.5366,9.1296 11.5043,9.1296ZM6.1045,9.4089C7.5698,9.7298 8.6666,11.0353 8.6666,12.5969L8.6666,14.7587H4.6666L1.7144,11.6667C2.3548,10.2875 3.7345,9.3333 5.3333,9.3333C5.5971,9.3333 5.855,9.3593 6.1045,9.4089ZM9.5501,10.611C9.8385,11.2121 10,11.8856 10,12.5969L10,14.7587H11.5043L14.4675,11.6667C13.8465,10.6685 12.7515,10.0057 11.5043,10.0057C10.7807,10.0057 10.1084,10.2288 9.5501,10.611Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M8,14.6667C11.6819,14.6667 14.6667,11.6819 14.6667,8C14.6667,4.3181 11.6819,1.3333 8,1.3333C4.3181,1.3333 1.3333,4.3181 1.3333,8C1.3333,11.6819 4.3181,14.6667 8,14.6667ZM8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
12
vector/src/main/res/drawable/ic_call_video_small.xml
Normal file
12
vector/src/main/res/drawable/ic_call_video_small.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="12dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="12">
|
||||
<path
|
||||
android:pathData="M0,3.6666C0,2.0098 1.3432,0.6666 3,0.6666H8.3333C9.9902,0.6666 11.3333,2.0098 11.3333,3.6666V8.3333C11.3333,9.9901 9.9902,11.3333 8.3333,11.3333H3C1.3431,11.3333 0,9.9902 0,8.3333V3.6666Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
<path
|
||||
android:pathData="M12.6666,3.9999L14.3753,2.633C15.03,2.1092 16,2.5754 16,3.4139V8.586C16,9.4245 15.03,9.8906 14.3753,9.3668L12.6666,7.9999V3.9999Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
@ -40,15 +40,23 @@
|
||||
android:background="@drawable/rounded_rect_shape_8"
|
||||
android:padding="8dp">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageCallStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_call_tile_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageVerificationRequestStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_verification_stub"
|
||||
tools:visibility="gone" />
|
||||
tools:layout_marginTop= "250dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageVerificationDoneStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
tools:layout_marginTop= "450dp"
|
||||
android:layout="@layout/item_timeline_event_status_tile_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemCallCreatorAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
tools:src="@tools:sample/avatars"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemCallCreatorNameTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:drawablePadding="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="@sample/matrix.json/data/displayName" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemCallKindTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="12sp"
|
||||
tools:text="@string/action_video_call" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemCallStatusTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:textColor="?attr/vctr_notice_secondary"
|
||||
android:textSize="13sp"
|
||||
tools:text="@string/video_call_in_progress" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/itemCallAcceptRejectViewGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:id="@+id/itemCallAcceptView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:minWidth="120dp"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemCallRejectView"
|
||||
app:layout_constraintTop_toTopOf="@id/itemCallRejectView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/itemCallRejectView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="120dp"
|
||||
style="@style/VectorButtonStyleDestructive"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemCallAcceptView"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
@ -2747,4 +2747,12 @@
|
||||
<string name="warning_unsaved_change_discard">Discard changes</string>
|
||||
|
||||
<string name="matrix_to_card_title">Matrix Link</string>
|
||||
|
||||
<string name="call_tile_you_started_call">You started a call</string>
|
||||
<string name="call_tile_other_started_call">%1$s started a call</string>
|
||||
<string name="call_tile_in_call">You\'re currently in this call</string>
|
||||
<string name="call_tile_you_declined">You declined this call %1$s</string>
|
||||
<string name="call_tile_other_declined">%1$s declined this call</string>
|
||||
<string name="call_tile_ended">This call has ended</string>
|
||||
<string name="call_tile_call_back">Call back</string>
|
||||
</resources>
|
||||
|
@ -135,11 +135,13 @@
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:textColor">@color/button_destructive_text_color_selector</item>
|
||||
<item name="drawableTint">@color/riotx_notice</item>
|
||||
</style>
|
||||
|
||||
<style name="VectorButtonStylePositive" parent="VectorButtonStyleDestructive">
|
||||
<item name="backgroundTint">@color/button_positive_background_selector</item>
|
||||
<item name="android:textColor">@color/button_positive_text_color_selector</item>
|
||||
<item name="drawableTint">@color/riotx_accent</item>
|
||||
</style>
|
||||
|
||||
<style name="VectorButtonStyleInlineBot" parent="VectorButtonStyleDestructive">
|
||||
|
Loading…
x
Reference in New Issue
Block a user