Merge pull request #685 from vector-im/feature/timeline_items

Feature/timeline items
This commit is contained in:
Benoit Marty 2019-11-15 16:11:37 +01:00 committed by GitHub
commit fe17050580
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 207 additions and 32 deletions

View file

@ -9,6 +9,7 @@ Improvements 🙌:
- Search reaction by name or keyword in emoji picker
- Handle code tags (#567)
- Support spoiler messages
- Support m.sticker and m.room.join_rules events in timeline
Other changes:
- Markdown set to off by default (#412)

View file

@ -0,0 +1,38 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
/**
* Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules
*/
enum class RoomJoinRules(val value: String) {
@Json(name = "public")
PUBLIC("public"),
@Json(name = "invite")
INVITE("invite"),
@Json(name = "knock")
KNOCK("knock"),
@Json(name = "private")
PRIVATE("private")
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Class representing the EventType.STATE_ROOM_JOIN_RULES state event content
*/
@JsonClass(generateAdapter = true)
data class RoomJoinRulesContent(
@Json(name = "join_rule") val joinRules: RoomJoinRules? = null
)

View file

@ -38,7 +38,7 @@ data class MessageImageContent(
/**
* Metadata about the image referred to in url.
*/
@Json(name = "info") val info: ImageInfo? = null,
@Json(name = "info") override val info: ImageInfo? = null,
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
@ -52,4 +52,4 @@ data class MessageImageContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent
) : MessageImageInfoContent

View file

@ -0,0 +1,25 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.api.session.room.model.message
/**
* A content with image information
*/
interface MessageImageInfoContent : MessageEncryptedContent {
val info: ImageInfo?
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true)
data class MessageStickerContent(
/**
* Set in local, not from server
*/
override val type: String = MessageType.MSGTYPE_STICKER_LOCAL,
/**
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
* or some kind of content description for accessibility e.g. 'image attachment'.
*/
@Json(name = "body") override val body: String,
/**
* Metadata about the image referred to in url.
*/
@Json(name = "info") override val info: ImageInfo? = null,
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
@Json(name = "url") override val url: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageImageInfoContent

View file

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
@ -99,8 +100,14 @@ fun TimelineEvent.getEditedEventId(): String? {
/**
* Get last MessageContent, after a possible edition
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
fun TimelineEvent.getLastMessageContent(): MessageContent? {
return if (root.getClearType() == EventType.STICKER) {
root.getClearContent().toModel<MessageStickerContent>()
} else {
annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
}
}
/**
* Get last Message body, after a possible edition

View file

@ -877,7 +877,7 @@ class RoomDetailFragment @Inject constructor(
vectorBaseActivity.notImplemented("encrypted message click")
}
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))

View file

@ -62,7 +62,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onEventVisible(event: TimelineEvent)
fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)

View file

@ -101,7 +101,7 @@ class MessageItemFactory @Inject constructor(
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
@ -157,7 +157,7 @@ class MessageItemFactory @Inject constructor(
return defaultItemFactory.create(text, informationData, highlight, callback)
}
private fun buildImageMessageItem(messageContent: MessageImageContent,
private fun buildImageMessageItem(messageContent: MessageImageInfoContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,

View file

@ -39,12 +39,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
val computedModel = try {
when (event.root.getClearType()) {
EventType.STICKER,
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
// State and call
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_JOIN_RULES,
EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
@ -65,8 +67,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
}
// Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback)
else -> {
Timber.v("Type ${event.root.getClearType()} not handled")
null

View file

@ -22,6 +22,8 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomJoinRules
import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
@ -38,6 +40,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) {
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
@ -58,6 +61,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
fun format(event: Event, senderName: String?): CharSequence? {
return when (val type = event.getClearType()) {
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
@ -220,4 +224,13 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
else -> null
}
}
private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
return when (content.joinRules) {
RoomJoinRules.INVITE -> stringProvider.getString(R.string.room_join_rules_invite, senderName)
RoomJoinRules.PUBLIC -> stringProvider.getString(R.string.room_join_rules_public, senderName)
else -> null
}
}
}

View file

@ -36,7 +36,8 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.STATE_ROOM_CREATE,
EventType.STATE_ROOM_TOMBSTONE
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_JOIN_RULES
)
val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(

View file

@ -18,10 +18,9 @@ package im.vector.riotx.features.home.room.list
import android.view.View
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -97,9 +96,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
latestFormattedEvent = if (latestEvent.root.isEncrypted()
&& latestEvent.root.mxDecryptionResult == null) {
stringProvider.getString(R.string.encrypted_message)
} else if (latestEvent.root.getClearType() == EventType.MESSAGE) {
} else if (latestEvent.root.getClearType() == EventType.MESSAGE || latestEvent.root.getClearType() == EventType.STICKER) {
val senderName = latestEvent.getDisambiguatedDisplayName()
val content = latestEvent.root.getClearContent()?.toModel<MessageContent>()
val content = latestEvent.getLastMessageContent()
val message = content?.body ?: ""
if (roomSummary.isDirect.not()) {
span {

View file

@ -19,4 +19,9 @@
<string name="help_long_click_on_room_for_more_options">Long click on a room to see more options</string>
<string name="room_join_rules_public">%1$s made the room public to whoever knows the link.</string>
<string name="room_join_rules_invite">%1$s made the room invite only.</string>
</resources>