diff --git a/changelog.d/5710.feature b/changelog.d/5710.feature new file mode 100644 index 0000000000..d9b043bc32 --- /dev/null +++ b/changelog.d/5710.feature @@ -0,0 +1 @@ +Live Location Sharing - Show message on start of a live \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 7766168297..ec377ee826 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -423,4 +423,5 @@ fun Event.getPollContent(): MessagePollContent? { return content.toModel() } -fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START +fun Event.supportsNotification() = + this.getClearType() in EventType.MESSAGE + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt index a3faee0568..106e76eafd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt @@ -18,12 +18,26 @@ package org.matrix.android.sdk.api.session.room.model.livelocation import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.message.LocationAsset import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType +import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class LiveLocationBeaconContent( + /** + * Local message type, not from server + */ + @Transient + override val msgType: String = MessageType.MSGTYPE_LIVE_LOCATION_STATE, + + @Json(name = "body") override val body: String = "", + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + /** * Indicates user's intent to share ephemeral location. */ @@ -43,8 +57,13 @@ data class LiveLocationBeaconContent( /** * Client side tracking of the last location */ - var lastLocationContent: MessageLiveLocationContent? = null -) { + var lastLocationContent: MessageLiveLocationContent? = null, + + /** + * Client side tracking of whether the beacon has timed out. + */ + var hasTimedOut: Boolean = false +) : MessageContent { fun getBestBeaconInfo() = beaconInfo ?: unstableBeaconInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index 280699cde8..106bf2e030 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -41,5 +41,6 @@ object MessageType { const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall" // Fake message types for live location events to be able to inherit them from MessageContent + const val MSGTYPE_LIVE_LOCATION_STATE = "org.matrix.android.sdk.livelocation.state" const val MSGTYPE_LIVE_LOCATION = "org.matrix.android.sdk.livelocation" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt index eaed9053ea..8f214e0f89 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt @@ -33,5 +33,5 @@ object RoomSummaryConstants { EventType.ENCRYPTED, EventType.STICKER, EventType.REACTION - ) + EventType.POLL_START + ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 1b01efc074..d70049a144 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.ReadReceipt +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent @@ -136,9 +137,10 @@ fun TimelineEvent.getEditedEventId(): String? { */ fun TimelineEvent.getLastMessageContent(): MessageContent? { return when (root.getClearType()) { - EventType.STICKER -> root.getClearContent().toModel() - in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + EventType.STICKER -> root.getClearContent().toModel() + in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 8ae203c2b3..899bce4c8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -57,6 +57,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor( val allEvents = (newJoinEvents + inviteEvents).filter { event -> when (event.type) { in EventType.POLL_START, + in EventType.STATE_ROOM_BEACON_INFO, EventType.MESSAGE, EventType.REDACTION, EventType.ENCRYPTED, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt index 2af536a4a6..95e196c762 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt @@ -41,6 +41,7 @@ internal class DefaultLiveLocationAggregationProcessor @Inject constructor() : L } // A beacon info state event has to be sent before sending location + // TODO handle missing check of m_relatesTo field var beaconInfoEntity: CurrentStateEventEntity? = null val eventTypesIterator = EventType.STATE_ROOM_BEACON_INFO.iterator() while (beaconInfoEntity == null && eventTypesIterator.hasNext()) { @@ -66,11 +67,11 @@ internal class DefaultLiveLocationAggregationProcessor @Inject constructor() : L // Check if beacon info is outdated if (isBeaconInfoOutdated(beaconInfoContent, content)) { Timber.v("## LIVE LOCATION. Beacon info has timeout") - return + beaconInfoContent.hasTimedOut = true + } else { + beaconInfoContent.lastLocationContent = content } - // Update last location info of the beacon state event - beaconInfoContent.lastLocationContent = content beaconInfoEntity.root?.content = ContentMapper.map(beaconInfoContent.toContent()) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt new file mode 100644 index 0000000000..9bc148a562 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 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.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_ +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent +import javax.inject.Inject + +class LiveLocationMessageItemFactory @Inject constructor( + private val dimensionConverter: DimensionConverter, + private val timelineMediaSizeProvider: TimelineMediaSizeProvider, + private val avatarSizeProvider: AvatarSizeProvider, +) { + + fun create( + liveLocationContent: LiveLocationBeaconContent, + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): VectorEpoxyModel<*>? { + // TODO handle location received and stopped states + return when { + isLiveRunning(liveLocationContent) -> buildStartLiveItem(highlight, attributes) + else -> null + } + } + + private fun isLiveRunning(liveLocationContent: LiveLocationBeaconContent): Boolean { + return liveLocationContent.getBestBeaconInfo()?.isLive.orFalse() && liveLocationContent.hasTimedOut.not() + } + + private fun buildStartLiveItem( + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): MessageLiveLocationStartItem { + val width = timelineMediaSizeProvider.getMaxSize().first + val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) + + return MessageLiveLocationStartItem_() + .attributes(attributes) + .mapWidth(width) + .mapHeight(height) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index dc2266b154..2729ce3b8a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -96,6 +96,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody @@ -145,6 +146,7 @@ class MessageItemFactory @Inject constructor( private val locationPinProvider: LocationPinProvider, private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, + private val liveLocationMessageItemFactory: LiveLocationMessageItemFactory, ) { // TODO inject this properly? @@ -212,6 +214,7 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } } + is LiveLocationBeaconContent -> liveLocationMessageItemFactory.create(messageContent, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } return messageItem?.apply { @@ -226,7 +229,7 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes, ): MessageLocationItem? { val width = timelineMediaSizeProvider.getMaxSize().first - val height = dimensionConverter.dpToPx(200) + val height = dimensionConverter.dpToPx(MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP) val locationUrl = locationContent.toLocationData()?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) @@ -774,5 +777,6 @@ class MessageItemFactory @Inject constructor( companion object { private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5 + const val MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP = 200 } } 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 f9d2613e27..b5d620658e 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 @@ -26,17 +26,19 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import timber.log.Timber import javax.inject.Inject -class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory, - private val encryptedItemFactory: EncryptedItemFactory, - private val noticeItemFactory: NoticeItemFactory, - private val defaultItemFactory: DefaultItemFactory, - private val encryptionItemFactory: EncryptionItemFactory, - private val roomCreateItemFactory: RoomCreateItemFactory, - private val widgetItemFactory: WidgetItemFactory, - private val verificationConclusionItemFactory: VerificationItemFactory, - private val callItemFactory: CallItemFactory, - private val decryptionFailureTracker: DecryptionFailureTracker, - private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { +class TimelineItemFactory @Inject constructor( + private val messageItemFactory: MessageItemFactory, + private val encryptedItemFactory: EncryptedItemFactory, + private val noticeItemFactory: NoticeItemFactory, + private val defaultItemFactory: DefaultItemFactory, + private val encryptionItemFactory: EncryptionItemFactory, + private val roomCreateItemFactory: RoomCreateItemFactory, + private val widgetItemFactory: WidgetItemFactory, + private val verificationConclusionItemFactory: VerificationItemFactory, + private val callItemFactory: CallItemFactory, + private val decryptionFailureTracker: DecryptionFailureTracker, + private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, +) { /** * Reminder: nextEvent is older and prevEvent is newer. @@ -75,16 +77,17 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_ALIASES, EventType.STATE_SPACE_CHILD, EventType.STATE_SPACE_PARENT, - EventType.STATE_ROOM_POWER_LEVELS -> { + EventType.STATE_ROOM_POWER_LEVELS -> { noticeItemFactory.create(params) } EventType.STATE_ROOM_WIDGET_LEGACY, - EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) - EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) + EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) + EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) // State room create - EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params) + EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params) + in EventType.STATE_ROOM_BEACON_INFO -> messageItemFactory.create(params) // Unhandled state event types - else -> { + else -> { // Should only happen when shouldShowHiddenEvents() settings is ON Timber.v("State event type ${event.root.type} not handled") defaultItemFactory.create(params) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index b83322dc9b..3c2bdb53ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -59,7 +59,7 @@ class DisplayableEventFormatter @Inject constructor( val senderName = timelineEvent.senderInfo.disambiguatedDisplayName return when (timelineEvent.root.getClearType()) { - EventType.MESSAGE -> { + EventType.MESSAGE -> { timelineEvent.getLastMessageContent()?.let { messageContent -> when (messageContent.msgType) { MessageType.MSGTYPE_TEXT -> { @@ -100,17 +100,17 @@ class DisplayableEventFormatter @Inject constructor( } } ?: span { } } - EventType.STICKER -> { + EventType.STICKER -> { simpleFormat(senderName, stringProvider.getString(R.string.send_a_sticker), appendAuthor) } - EventType.REACTION -> { + EventType.REACTION -> { timelineEvent.root.getClearContent().toModel()?.relatesTo?.let { val emojiSpanned = emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key)) simpleFormat(senderName, emojiSpanned, appendAuthor) } ?: span { } } EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE -> { + EventType.KEY_VERIFICATION_DONE -> { // cancel and done can appear in timeline, so should have representation simpleFormat(senderName, stringProvider.getString(R.string.sent_verification_conclusion), appendAuthor) } @@ -119,20 +119,23 @@ class DisplayableEventFormatter @Inject constructor( EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_READY, - EventType.CALL_CANDIDATES -> { + EventType.CALL_CANDIDATES -> { span { } } - in EventType.POLL_START -> { + in EventType.POLL_START -> { timelineEvent.root.getClearContent().toModel(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: stringProvider.getString(R.string.sent_a_poll) } - in EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { stringProvider.getString(R.string.poll_response_room_list_preview) } - in EventType.POLL_END -> { + in EventType.POLL_END -> { stringProvider.getString(R.string.poll_end_room_list_preview) } - else -> { + in EventType.STATE_ROOM_BEACON_INFO -> { + simpleFormat(senderName, stringProvider.getString(R.string.sent_live_location), appendAuthor) + } + else -> { span { text = noticeEventFormatter.format(timelineEvent, isDm) ?: "" textStyle = "italic" @@ -167,7 +170,7 @@ class DisplayableEventFormatter @Inject constructor( } return when (event.getClearType()) { - EventType.MESSAGE -> { + EventType.MESSAGE -> { (event.getClearContent().toModel() as? MessageContent)?.let { messageContent -> when (messageContent.msgType) { MessageType.MSGTYPE_TEXT -> { @@ -208,25 +211,28 @@ class DisplayableEventFormatter @Inject constructor( } } ?: span { } } - EventType.STICKER -> { + EventType.STICKER -> { stringProvider.getString(R.string.send_a_sticker) } - EventType.REACTION -> { + EventType.REACTION -> { event.getClearContent().toModel()?.relatesTo?.let { emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key)) } ?: span { } } - in EventType.POLL_START -> { + in EventType.POLL_START -> { event.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question ?: stringProvider.getString(R.string.sent_a_poll) } - in EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { stringProvider.getString(R.string.poll_response_room_list_preview) } - in EventType.POLL_END -> { + in EventType.POLL_END -> { stringProvider.getString(R.string.poll_end_room_list_preview) } - else -> { + in EventType.STATE_ROOM_BEACON_INFO -> { + stringProvider.getString(R.string.sent_live_location) + } + else -> { span { } } 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 96a2ca4609..1736b20d44 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 @@ -50,8 +50,8 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_JOIN_RULES, EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL - ) + EventType.POLL_START + EventType.KEY_VERIFICATION_CANCEL, + ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO } fun TimelineEvent.canBeMerged(): Boolean { @@ -71,7 +71,7 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean { EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_POWER_LEVELS, EventType.STATE_ROOM_ENCRYPTION -> true - EventType.STATE_ROOM_MEMBER -> { + EventType.STATE_ROOM_MEMBER -> { // Keep only room member events regarding the room creator (when he joined the room), // but exclude events where the room creator invite others, or where others join roomCreatorUserId != null && root.stateKey == roomCreatorUserId diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt new file mode 100644 index 0000000000..390db0ef50 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022 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.graphics.drawable.ColorDrawable +import android.widget.ImageView +import androidx.core.view.updateLayoutParams +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import im.vector.app.R +import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners +import im.vector.app.features.themes.ThemeUtils + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageLiveLocationStartItem : AbsMessageItem() { + + @EpoxyAttribute + var mapWidth: Int = 0 + + @EpoxyAttribute + var mapHeight: Int = 0 + + override fun bind(holder: Holder) { + super.bind(holder) + renderSendState(holder.view, null) + bindMap(holder) + bindBottomBanner(holder) + } + + private fun bindMap(holder: Holder) { + val messageLayout = attributes.informationData.messageLayout + val mapCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { + messageLayout.cornersRadius.granularRoundedCorners() + } else { + RoundedCorners(getDefaultLayoutCornerRadiusInDp(holder)) + } + holder.noLocationMapImageView.updateLayoutParams { + width = mapWidth + height = mapHeight + } + GlideApp.with(holder.noLocationMapImageView) + .load(R.drawable.bg_no_location_map) + .transform(mapCornerTransformation) + .into(holder.noLocationMapImageView) + } + + private fun bindBottomBanner(holder: Holder) { + val messageLayout = attributes.informationData.messageLayout + val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { + GranularRoundedCorners(0f, 0f, messageLayout.cornersRadius.bottomEndRadius, messageLayout.cornersRadius.bottomStartRadius) + } else { + val bottomCornerRadius = getDefaultLayoutCornerRadiusInDp(holder).toFloat() + GranularRoundedCorners(0f, 0f, bottomCornerRadius, bottomCornerRadius) + } + GlideApp.with(holder.bannerImageView) + .load(ColorDrawable(ThemeUtils.getColor(holder.bannerImageView.context, R.attr.colorSurface))) + .transform(imageCornerTransformation) + .into(holder.bannerImageView) + } + + private fun getDefaultLayoutCornerRadiusInDp(holder: Holder): Int { + val dimensionConverter = DimensionConverter(holder.view.resources) + return dimensionConverter.dpToPx(8) + } + + override fun getViewStubId() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val bannerImageView by bind(R.id.locationLiveStartBanner) + val noLocationMapImageView by bind(R.id.locationLiveStartMap) + } + + companion object { + private const val STUB_ID = R.id.messageContentLiveLocationStartStub + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index c0e668e013..ae9b004f6c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -38,19 +38,20 @@ sealed interface TimelineMessageLayout : Parcelable { @Parcelize data class Bubble( - override val showAvatar: Boolean, - override val showDisplayName: Boolean, - override val showTimestamp: Boolean = true, - val addTopMargin: Boolean = false, - val isIncoming: Boolean, - val isPseudoBubble: Boolean, - val cornersRadius: CornersRadius, - val timestampAsOverlay: Boolean, - override val layoutRes: Int = if (isIncoming) { - R.layout.item_timeline_event_bubble_incoming_base - } else { - R.layout.item_timeline_event_bubble_outgoing_base - }, + override val showAvatar: Boolean, + override val showDisplayName: Boolean, + override val showTimestamp: Boolean = true, + val addTopMargin: Boolean = false, + val isIncoming: Boolean, + val isPseudoBubble: Boolean, + val cornersRadius: CornersRadius, + val timestampInsideMessage: Boolean, + val addMessageOverlay: Boolean, + override val layoutRes: Int = if (isIncoming) { + R.layout.item_timeline_event_bubble_incoming_base + } else { + R.layout.item_timeline_event_bubble_outgoing_base + }, ) : TimelineMessageLayout { @Parcelize diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 04430bf69f..f2334e5a4f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -46,7 +46,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess EventType.MESSAGE, EventType.ENCRYPTED, EventType.STICKER - ) + EventType.POLL_START + ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO // Can't be rendered in bubbles, so get back to default layout private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf( @@ -58,10 +58,13 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_STICKER_LOCAL, - MessageType.MSGTYPE_EMOTE + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_LIVE_LOCATION_STATE, ) - private val MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY = setOf( - MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO + private val MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE = setOf( + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_LIVE_LOCATION_STATE, ) } @@ -70,7 +73,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess } private val isRTL: Boolean by lazy { - localeProvider.isRTL() + localeProvider.isRTL() } fun create(params: TimelineItemFactoryParams): TimelineMessageLayout { @@ -123,7 +126,8 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess isIncoming = !isSentByMe, cornersRadius = cornersRadius, isPseudoBubble = messageContent.isPseudoBubble(), - timestampAsOverlay = messageContent.timestampAsOverlay() + timestampInsideMessage = messageContent.timestampInsideMessage(), + addMessageOverlay = messageContent.shouldAddMessageOverlay(), ) } else { buildModernLayout(showInformation) @@ -139,10 +143,18 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess return this.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT } - private fun MessageContent?.timestampAsOverlay(): Boolean { + private fun MessageContent?.timestampInsideMessage(): Boolean { if (this == null) return false if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() - return this.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY + return this.msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE + } + + private fun MessageContent?.shouldAddMessageOverlay(): Boolean { + return when { + this == null || msgType == MessageType.MSGTYPE_LIVE_LOCATION_STATE -> false + msgType == MessageType.MSGTYPE_LOCATION -> vectorPreferences.labsRenderLocationsInTimeline() + else -> msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE + } } private fun TimelineEvent.shouldBuildBubbleLayout(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 954aa0bf34..87ed9243a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -43,9 +43,9 @@ import im.vector.app.features.themes.ThemeUtils import timber.log.Timber class MessageBubbleView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, ) : RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer { private var isIncoming: Boolean = false @@ -89,21 +89,21 @@ class MessageBubbleView @JvmOverloads constructor( outlineProvider = ViewOutlineProvider.BACKGROUND clipToOutline = true background = RippleDrawable( - ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT), - bubbleDrawable, - rippleMaskDrawable) + ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT), + bubbleDrawable, + rippleMaskDrawable) } } override fun renderMessageLayout(messageLayout: TimelineMessageLayout) { (messageLayout as? TimelineMessageLayout.Bubble) - ?.updateDrawables() - ?.setConstraintsAndColor() - ?.toggleMessageOverlay() - ?.setPadding() - ?.setMargins() - ?.setAdditionalTopSpace() - ?: Timber.v("Can't render messageLayout $messageLayout") + ?.updateDrawables() + ?.setConstraints() + ?.toggleMessageOverlay() + ?.setPadding() + ?.setMargins() + ?.setAdditionalTopSpace() + ?: Timber.v("Can't render messageLayout $messageLayout") } private fun TimelineMessageLayout.Bubble.updateDrawables() = apply { @@ -121,17 +121,13 @@ class MessageBubbleView @JvmOverloads constructor( rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel } - private fun TimelineMessageLayout.Bubble.setConstraintsAndColor() = apply { + private fun TimelineMessageLayout.Bubble.setConstraints() = apply { ConstraintSet().apply { clone(views.bubbleView) clear(R.id.viewStubContainer, ConstraintSet.END) - if (timestampAsOverlay) { - val timeColor = ContextCompat.getColor(context, R.color.palette_white) - views.messageTimeView.setTextColor(timeColor) + if (timestampInsideMessage) { connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0) } else { - val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary) - views.messageTimeView.setTextColor(timeColor) connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0) } applyTo(views.bubbleView) @@ -139,16 +135,20 @@ class MessageBubbleView @JvmOverloads constructor( } private fun TimelineMessageLayout.Bubble.toggleMessageOverlay() = apply { - if (timestampAsOverlay) { + if (addMessageOverlay) { + val timeColor = ContextCompat.getColor(context, R.color.palette_white) + views.messageTimeView.setTextColor(timeColor) views.messageOverlayView.isVisible = true (views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = cornersRadius.toFloatArray() } else { + val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary) + views.messageTimeView.setTextColor(timeColor) views.messageOverlayView.isVisible = false } } private fun TimelineMessageLayout.Bubble.setPadding() = apply { - if (isPseudoBubble && timestampAsOverlay) { + if (isPseudoBubble && timestampInsideMessage) { views.viewStubContainer.root.setPadding(0, 0, 0, 0) } else { views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 3c9b985df5..d8229441d0 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -155,7 +155,8 @@ class NotifiableEventResolver @Inject constructor( // only convert encrypted messages to NotifiableMessageEvents when (event.root.getClearType()) { EventType.MESSAGE, - in EventType.POLL_START -> { + in EventType.POLL_START, + in EventType.STATE_ROOM_BEACON_INFO -> { val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderInfo.disambiguatedDisplayName @@ -187,7 +188,7 @@ class NotifiableEventResolver @Inject constructor( soundName = null ) } - else -> null + else -> null } } } diff --git a/vector/src/main/res/drawable-hdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-hdpi/bg_no_location_map.webp new file mode 100644 index 0000000000..23a45700f0 Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/bg_no_location_map.webp differ diff --git a/vector/src/main/res/drawable-mdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-mdpi/bg_no_location_map.webp new file mode 100644 index 0000000000..a6130fba78 Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/bg_no_location_map.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp new file mode 100644 index 0000000000..e908191371 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp new file mode 100644 index 0000000000..e062178367 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp new file mode 100644 index 0000000000..8b110d33fe Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp differ diff --git a/vector/src/main/res/layout/item_timeline_event_live_location_start_stub.xml b/vector/src/main/res/layout/item_timeline_event_live_location_start_stub.xml new file mode 100644 index 0000000000..b81a6cc0e9 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_live_location_start_stub.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml index fce01ea074..355d5fa7fe 100644 --- a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml +++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml @@ -59,6 +59,11 @@ android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_location_stub" /> + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b39870832d..551983637c 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2121,6 +2121,7 @@ Reacted with: %s Verification Conclusion Shared their location + Shared their live location Waiting… %s cancelled @@ -2998,6 +2999,7 @@ Render user locations in the timeline Failed to load map Live location enabled + Loading live location… Stop ${app_name} Live Location Location sharing is in progress