diff --git a/CHANGES.md b/CHANGES.md index 757a4ab544..34c696f786 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,12 +6,11 @@ Features ✨: Improvements 🙌: - VoIP : new tiles in timeline - -Bugfix 🐛: - - VoIP : fix audio devices output + - Create a WidgetItemFactory and use it for better rendering of Jitsi widget change (video conference) - Open image from URL Preview (#2705) Bugfix 🐛: + - VoIP : fix audio devices output - Bug in WidgetContent.computeURL() (#2767) - Duplicate thumbs | Mobile reactions for 👍 and 👎 are not the same as web (#2776) - Join room by alias other federation error (#2778) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt index 1b4368e3da..9b73136fc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.room.sender +import org.matrix.android.sdk.internal.util.replaceSpaceChars + data class SenderInfo( val userId: String, /** @@ -27,8 +29,9 @@ data class SenderInfo( ) { val disambiguatedDisplayName: String get() = when { - displayName.isNullOrBlank() -> userId - isUniqueDisplayName -> displayName - else -> "$displayName ($userId)" + displayName == null -> userId + displayName.replaceSpaceChars().isBlank() -> "$displayName ($userId)" + isUniqueDisplayName -> displayName + else -> "$displayName ($userId)" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index ecfbe311f1..2fabca4be8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -71,3 +71,10 @@ fun String.caseInsensitiveFind(subString: String): Boolean { return false } + +internal val spaceChars = "[\u00A0\u2000-\u200B\u2800\u3000]".toRegex() + +/** + * Strip all the UTF-8 chars which are actually spaces + */ +internal fun String.replaceSpaceChars() = replace(spaceChars, "") diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 7a0fe1d735..26b9bc19d9 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -126,6 +126,13 @@ %1$s modified %2$s widget You modified %1$s widget + Video conference started by %1$s + You started video conference + Video conference ended by %1$s + You ended video conference + Video conference modified by %1$s + You modified video conference + Admin Moderator Default 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 bd045e166a..c9558da1d2 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 @@ -20,7 +20,6 @@ import im.vector.app.core.epoxy.EmptyItem_ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import timber.log.Timber @@ -32,7 +31,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val defaultItemFactory: DefaultItemFactory, private val encryptionItemFactory: EncryptionItemFactory, private val roomCreateItemFactory: RoomCreateItemFactory, - private val roomSummariesHolder: RoomSummariesHolder, + private val widgetItemFactory: WidgetItemFactory, private val verificationConclusionItemFactory: VerificationItemFactory, private val callItemFactory: CallItemFactory, private val userPreferencesProvider: UserPreferencesProvider) { @@ -59,11 +58,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, - EventType.STATE_ROOM_WIDGET_LEGACY, - EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_POWER_LEVELS, EventType.REACTION, EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) + EventType.STATE_ROOM_WIDGET_LEGACY, + EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt new file mode 100644 index 0000000000..8d8f42b2d1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2021 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.ActiveSessionDataSource +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.resources.StringProvider +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.WidgetTileTimelineItem +import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem_ +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.widgets.model.WidgetContent +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import javax.inject.Inject + +class WidgetItemFactory @Inject constructor( + private val sp: StringProvider, + private val roomSummaryHolder: RoomSummaryHolder, + private val messageItemAttributesFactory: MessageItemAttributesFactory, + private val informationDataFactory: MessageInformationDataFactory, + private val noticeItemFactory: NoticeItemFactory, + private val avatarSizeProvider: AvatarSizeProvider, + private val activeSessionDataSource: ActiveSessionDataSource +) { + private val currentUserId: String? + get() = activeSessionDataSource.currentValue?.orNull()?.myUserId + + private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId + + fun create(event: TimelineEvent, + highlight: Boolean, + callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null + val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel() + + return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { + WidgetType.Jitsi -> createJitsiItem(event, callback, widgetContent, previousWidgetContent) + // There is lot of other widget types we could improve here + else -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + } + } + + private fun createJitsiItem(timelineEvent: TimelineEvent, + callback: TimelineEventController.Callback?, + widgetContent: WidgetContent, + previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { + val informationData = informationDataFactory.create(timelineEvent, null) + val attributes = messageItemAttributesFactory.create(null, informationData, callback) + + val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName + val message = if (widgetContent.isActive()) { + val widgetName = widgetContent.getHumanName() + if (previousWidgetContent?.isActive().orFalse()) { + // Widget has been modified + if (timelineEvent.root.isSentByCurrentUser()) { + sp.getString(R.string.notice_widget_jitsi_modified_by_you, widgetName) + } else { + sp.getString(R.string.notice_widget_jitsi_modified, disambiguatedDisplayName, widgetName) + } + } else { + // Widget has been added + if (timelineEvent.root.isSentByCurrentUser()) { + sp.getString(R.string.notice_widget_jitsi_added_by_you, widgetName) + } else { + sp.getString(R.string.notice_widget_jitsi_added, disambiguatedDisplayName, widgetName) + } + } + } else { + // Widget has been removed + val widgetName = previousWidgetContent?.getHumanName() + if (timelineEvent.root.isSentByCurrentUser()) { + sp.getString(R.string.notice_widget_jitsi_removed_by_you, widgetName) + } else { + sp.getString(R.string.notice_widget_jitsi_removed, disambiguatedDisplayName, widgetName) + } + } + + return WidgetTileTimelineItem_() + .attributes( + WidgetTileTimelineItem.Attributes( + title = message, + drawableStart = R.drawable.ic_video, + informationData = informationData, + avatarRenderer = attributes.avatarRenderer, + messageColorProvider = attributes.messageColorProvider, + itemLongClickListener = attributes.itemLongClickListener, + itemClickListener = attributes.itemClickListener, + reactionPillCallback = attributes.reactionPillCallback, + readReceiptsCallback = attributes.readReceiptsCallback, + emojiTypeFace = attributes.emojiTypeFace + ) + ) + .leftGuideline(avatarSizeProvider.leftGuideline) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 9b45adbe99..f36f91a9d9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -143,12 +143,14 @@ class NoticeEventFormatter @Inject constructor( return if (widgetContent.isActive()) { val widgetName = widgetContent.getHumanName() if (previousWidgetContent?.isActive().orFalse()) { + // Widget has been modified if (event.isSentByCurrentUser()) { sp.getString(R.string.notice_widget_modified_by_you, widgetName) } else { sp.getString(R.string.notice_widget_modified, disambiguatedDisplayName, widgetName) } } else { + // Widget has been added if (event.isSentByCurrentUser()) { sp.getString(R.string.notice_widget_added_by_you, widgetName) } else { @@ -156,6 +158,7 @@ class NoticeEventFormatter @Inject constructor( } } } else { + // Widget has been removed val widgetName = previousWidgetContent?.getHumanName() if (event.isSentByCurrentUser()) { sp.getString(R.string.notice_widget_removed_by_you, widgetName) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt new file mode 100644 index 0000000000..33f59ca22a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2021 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.annotation.SuppressLint +import android.graphics.Typeface +import android.view.View +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.view.updateLayoutParams +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.MessageColorProvider +import im.vector.app.features.home.room.detail.timeline.TimelineEventController + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) +abstract class WidgetTileTimelineItem : AbsBaseMessageItem() { + + override val baseAttributes: AbsBaseMessageItem.Attributes + get() = attributes + + @EpoxyAttribute + lateinit var attributes: Attributes + + override fun getViewType() = STUB_ID + + @SuppressLint("SetTextI18n") + override fun bind(holder: Holder) { + super.bind(holder) + holder.endGuideline.updateLayoutParams { + this.marginEnd = leftGuideline + } + + holder.titleView.text = attributes.title + holder.titleView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(holder.view.context, attributes.drawableStart), + null, null, null + ) + + renderSendState(holder.view, null, holder.failedToSendIndicator) + } + + class Holder : AbsBaseMessageItem.Holder(STUB_ID) { + val titleView by bind(R.id.itemWidgetTitle) + val endGuideline by bind(R.id.messageEndGuideline) + val failedToSendIndicator by bind(R.id.messageFailToSendIndicator) + } + + companion object { + private const val STUB_ID = R.id.messageWidgetStub + } + + /** + * This class holds all the common attributes for timeline items. + */ + data class Attributes( + val title: CharSequence, + @DrawableRes + val drawableStart: Int, + 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, + val emojiTypeFace: Typeface? = null + ) : AbsBaseMessageItem.Attributes +} diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index 4ca93898cd..11b8832c94 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -83,7 +83,7 @@ abstract class BaseAttachmentProvider( val dateString = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME) overlayView?.updateWith( counter = stringProvider.getString(R.string.attachment_viewer_item_x_of_y, position + 1, getItemCount()), - senderInfo = "${timelineEvent.senderInfo.displayName} $dateString" + senderInfo = "${timelineEvent.senderInfo.disambiguatedDisplayName} $dateString" ) overlayView?.views?.overlayVideoControlsGroup?.isVisible = timelineEvent.root.isVideoMessage() } else { diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml index 005e5a280a..e68f962a44 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_state.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml @@ -60,6 +60,13 @@ android:layout="@layout/item_timeline_event_status_tile_stub" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/layout/item_timeline_event_widget_stub.xml b/vector/src/main/res/layout/item_timeline_event_widget_stub.xml new file mode 100644 index 0000000000..573fecc4e8 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_widget_stub.xml @@ -0,0 +1,23 @@ + + + + + + + +