mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 09:25:49 +03:00
Merge pull request #5722 from vector-im/feature/mna/PSF-883-start-live-message
[Location sharing] - Show message on start of a live
This commit is contained in:
commit
c832c1b848
26 changed files with 376 additions and 90 deletions
1
changelog.d/5710.feature
Normal file
1
changelog.d/5710.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Live Location Sharing - Show message on start of a live
|
|
@ -423,4 +423,5 @@ fun Event.getPollContent(): MessagePollContent? {
|
|||
return content.toModel<MessagePollContent>()
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -33,5 +33,5 @@ object RoomSummaryConstants {
|
|||
EventType.ENCRYPTED,
|
||||
EventType.STICKER,
|
||||
EventType.REACTION
|
||||
) + EventType.POLL_START
|
||||
) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
|
||||
}
|
||||
|
|
|
@ -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<MessageStickerContent>()
|
||||
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<LiveLocationBeaconContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<ReactionContent>()?.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<MessagePollContent>(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<ReactionContent>()?.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<MessagePollContent>(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 {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<MessageLiveLocationStartItem.Holder>() {
|
||||
|
||||
@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<ImageView>(R.id.locationLiveStartBanner)
|
||||
val noLocationMapImageView by bind<ImageView>(R.id.locationLiveStartMap)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentLiveLocationStartStub
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
BIN
vector/src/main/res/drawable-hdpi/bg_no_location_map.webp
Normal file
BIN
vector/src/main/res/drawable-hdpi/bg_no_location_map.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 952 B |
BIN
vector/src/main/res/drawable-mdpi/bg_no_location_map.webp
Normal file
BIN
vector/src/main/res/drawable-mdpi/bg_no_location_map.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 638 B |
BIN
vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp
Normal file
BIN
vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp
Normal file
BIN
vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp
Normal file
BIN
vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<!-- Size will be overrode -->
|
||||
<ImageView
|
||||
android:id="@+id/locationLiveStartMap"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="200dp"
|
||||
android:contentDescription="@string/a11y_static_map_image"
|
||||
android:src="@drawable/bg_no_location_map"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/locationLiveStartBanner"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0.85"
|
||||
android:src="?colorSurface"
|
||||
app:layout_constraintBottom_toBottomOf="@id/locationLiveStartMap"
|
||||
app:layout_constraintEnd_toEndOf="@id/locationLiveStartMap"
|
||||
app:layout_constraintStart_toStartOf="@id/locationLiveStartMap"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/locationLiveStartIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:background="@drawable/circle"
|
||||
android:backgroundTint="?vctr_content_quaternary"
|
||||
android:padding="3dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/locationLiveStartBanner"
|
||||
app:layout_constraintStart_toStartOf="@id/locationLiveStartBanner"
|
||||
app:layout_constraintTop_toTopOf="@id/locationLiveStartBanner"
|
||||
app:srcCompat="@drawable/ic_attachment_location_live_white"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/locationLiveStartTitle"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:text="@string/location_share_live_started"
|
||||
android:textColor="?vctr_content_tertiary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/locationLiveStartBanner"
|
||||
app:layout_constraintStart_toEndOf="@id/locationLiveStartIcon"
|
||||
app:layout_constraintTop_toTopOf="@id/locationLiveStartBanner" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/locationLiveStartLoader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminateTint="?vctr_content_quaternary"
|
||||
app:layout_constraintBottom_toTopOf="@id/locationLiveStartBanner"
|
||||
app:layout_constraintEnd_toEndOf="@id/locationLiveStartMap"
|
||||
app:layout_constraintStart_toStartOf="@id/locationLiveStartMap"
|
||||
app:layout_constraintTop_toTopOf="@id/locationLiveStartMap" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -59,6 +59,11 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_location_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentLiveLocationStartStub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_live_location_start_stub" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
|
|
@ -2121,6 +2121,7 @@
|
|||
<string name="sent_a_reaction">Reacted with: %s</string>
|
||||
<string name="sent_verification_conclusion">Verification Conclusion</string>
|
||||
<string name="sent_location">Shared their location</string>
|
||||
<string name="sent_live_location">Shared their live location</string>
|
||||
|
||||
<string name="verification_request_waiting">Waiting…</string>
|
||||
<string name="verification_request_other_cancelled">%s cancelled</string>
|
||||
|
@ -2998,6 +2999,7 @@
|
|||
<string name="labs_render_locations_in_timeline">Render user locations in the timeline</string>
|
||||
<string name="location_timeline_failed_to_load_map">Failed to load map</string>
|
||||
<string name="location_share_live_enabled">Live location enabled</string>
|
||||
<string name="location_share_live_started">Loading live location…</string>
|
||||
<string name="location_share_live_stop">Stop</string>
|
||||
<string name="live_location_sharing_notification_title">${app_name} Live Location</string>
|
||||
<string name="live_location_sharing_notification_description">Location sharing is in progress</string>
|
||||
|
|
Loading…
Reference in a new issue