mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Handle permalink click
This commit is contained in:
parent
b1e009f8b4
commit
76ade2957e
30 changed files with 461 additions and 140 deletions
|
@ -24,7 +24,7 @@ import im.vector.matrix.android.api.session.events.model.Event
|
|||
*/
|
||||
object PermalinkFactory {
|
||||
|
||||
private val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
|
||||
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
|
||||
|
||||
/**
|
||||
* Creates a permalink for an event.
|
||||
|
|
|
@ -36,12 +36,20 @@ object PermalinkParser {
|
|||
* Turns an uri to a [PermalinkData]
|
||||
*/
|
||||
fun parse(uri: Uri): PermalinkData {
|
||||
if (!uri.toString().startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) {
|
||||
return PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
|
||||
val fragment = uri.fragment
|
||||
if (fragment.isNullOrEmpty()) {
|
||||
return PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
|
||||
val indexOfQuery = fragment.indexOf("?")
|
||||
val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment
|
||||
|
||||
// we are limiting to 2 params
|
||||
val params = fragment
|
||||
val params = safeFragment
|
||||
.split(MatrixPatterns.SEP_REGEX.toRegex())
|
||||
.filter { it.isNotEmpty() }
|
||||
.take(2)
|
||||
|
|
|
@ -32,7 +32,7 @@ package im.vector.matrix.android.api.session.room.timeline
|
|||
*/
|
||||
interface Timeline {
|
||||
|
||||
var listener: Timeline.Listener?
|
||||
var listener: Listener?
|
||||
|
||||
/**
|
||||
* This should be called before any other method after creating the timeline. It ensures the underlying database is open
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* 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.timeline
|
||||
|
||||
import androidx.paging.PagedList
|
||||
|
||||
/**
|
||||
* This data class is a holder for timeline data.
|
||||
* It's returned by [TimelineService]
|
||||
*/
|
||||
data class TimelineData(
|
||||
|
||||
/**
|
||||
* The [PagedList] of [TimelineEvent] to usually be render in a RecyclerView.
|
||||
*/
|
||||
val events: PagedList<TimelineEvent>,
|
||||
|
||||
/**
|
||||
* True if Timeline is currently paginating forward on server
|
||||
*/
|
||||
val isLoadingForward: Boolean = false,
|
||||
|
||||
/**
|
||||
* True if Timeline is currently paginating backward on server
|
||||
*/
|
||||
val isLoadingBackward: Boolean = false
|
||||
)
|
|
@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.send.SendState
|
|||
|
||||
/**
|
||||
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
|
||||
* This class is used by [TimelineService] through [TimelineData]
|
||||
* This class is used by [TimelineService]
|
||||
* Users can also enrich it with metadata.
|
||||
*/
|
||||
data class TimelineEvent(
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* 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.timeline
|
||||
|
||||
|
||||
interface TimelineEventInterceptor {
|
||||
|
||||
fun canEnrich(event: TimelineEvent): Boolean
|
||||
|
||||
fun enrich(event: TimelineEvent)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.riotredesign.core.platform
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Checkable
|
||||
|
||||
class CheckableView : View, Checkable {
|
||||
|
||||
private var mChecked = false
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun isChecked(): Boolean {
|
||||
return mChecked
|
||||
}
|
||||
|
||||
override fun setChecked(b: Boolean) {
|
||||
if (b != mChecked) {
|
||||
mChecked = b
|
||||
refreshDrawableState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toggle() {
|
||||
isChecked = !mChecked
|
||||
}
|
||||
|
||||
public override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
||||
val drawableState = super.onCreateDrawableState(extraSpace + 1)
|
||||
if (isChecked) {
|
||||
mergeDrawableStates(drawableState, CHECKED_STATE_SET)
|
||||
}
|
||||
return drawableState
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ class HomeModule {
|
|||
}
|
||||
|
||||
scope(HOME_SCOPE) {
|
||||
PermalinkHandler(get())
|
||||
PermalinkHandler(get(), get())
|
||||
}
|
||||
|
||||
// Fragment scopes
|
||||
|
|
|
@ -20,36 +20,68 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||
import im.vector.riotredesign.core.utils.openUrlInExternalBrowser
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.features.navigation.Navigator
|
||||
|
||||
class PermalinkHandler(private val navigator: Navigator) {
|
||||
class PermalinkHandler(private val session: Session,
|
||||
private val navigator: Navigator) {
|
||||
|
||||
fun launch(context: Context, deepLink: String?) {
|
||||
fun launch(context: Context, deepLink: String?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
|
||||
val uri = deepLink?.let { Uri.parse(it) }
|
||||
launch(context, uri)
|
||||
return launch(context, uri, navigateToRoomInterceptor)
|
||||
}
|
||||
|
||||
fun launch(context: Context, deepLink: Uri?) {
|
||||
fun launch(context: Context, deepLink: Uri?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
|
||||
if (deepLink == null) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
when (val permalinkData = PermalinkParser.parse(deepLink)) {
|
||||
|
||||
return when (val permalinkData = PermalinkParser.parse(deepLink)) {
|
||||
is PermalinkData.EventLink -> {
|
||||
navigator.openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId)
|
||||
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias, permalinkData.eventId) != true) {
|
||||
openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
navigator.openRoom(context, permalinkData.roomIdOrAlias)
|
||||
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias) != true) {
|
||||
openRoom(context, permalinkData.roomIdOrAlias)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
is PermalinkData.GroupLink -> {
|
||||
navigator.openGroupDetail(permalinkData.groupId, context)
|
||||
true
|
||||
}
|
||||
is PermalinkData.UserLink -> {
|
||||
navigator.openUserDetail(permalinkData.userId, context)
|
||||
true
|
||||
}
|
||||
is PermalinkData.FallbackLink -> {
|
||||
openUrlInExternalBrowser(context, permalinkData.uri)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open room either joined, or not unknown
|
||||
*/
|
||||
private fun openRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) {
|
||||
if (session.getRoom(roomIdOrAlias) != null) {
|
||||
navigator.openRoom(context, roomIdOrAlias, eventId)
|
||||
} else {
|
||||
navigator.openNotJoinedRoom(context, roomIdOrAlias, eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface NavigateToRoomInterceptor {
|
||||
|
||||
/**
|
||||
* Return true if the navigation has been intercepted
|
||||
*/
|
||||
fun navToRoom(roomId: String, eventId: String? = null): Boolean
|
||||
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ sealed class RoomDetailActions {
|
|||
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
|
||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions()
|
||||
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
||||
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
|
||||
object AcceptInvite : RoomDetailActions()
|
||||
object RejectInvite : RoomDetailActions()
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ import butterknife.BindView
|
|||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -65,6 +66,7 @@ import im.vector.riotredesign.core.dialogs.DialogListItem
|
|||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.riotredesign.core.extensions.hideKeyboard
|
||||
import im.vector.riotredesign.core.extensions.observeEvent
|
||||
import im.vector.riotredesign.core.extensions.setTextOrHide
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.core.utils.*
|
||||
|
@ -72,10 +74,7 @@ import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandP
|
|||
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
|
||||
import im.vector.riotredesign.features.command.Command
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.PermalinkHandler
|
||||
import im.vector.riotredesign.features.home.getColorFromUserId
|
||||
import im.vector.riotredesign.features.home.*
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
||||
|
@ -170,6 +169,7 @@ class RoomDetailFragment :
|
|||
private val permalinkHandler: PermalinkHandler by inject()
|
||||
|
||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_room_detail
|
||||
|
||||
|
@ -199,6 +199,11 @@ class RoomDetailFragment :
|
|||
handleActions(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.navigateToEvent.observeEvent(this) {
|
||||
//
|
||||
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.selectSubscribe(
|
||||
RoomDetailViewState::sendMode,
|
||||
RoomDetailViewState::selectedEvent,
|
||||
|
@ -297,12 +302,14 @@ class RoomDetailFragment :
|
|||
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
|
||||
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.itemAnimator = null
|
||||
recyclerView.setHasFixedSize(true)
|
||||
timelineEventController.addModelBuildListener {
|
||||
it.dispatchTo(stateRestorer)
|
||||
it.dispatchTo(scrollOnNewMessageCallback)
|
||||
it.dispatchTo(scrollOnHighlightedEventCallback)
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(
|
||||
|
@ -467,7 +474,7 @@ class RoomDetailFragment :
|
|||
val summary = state.asyncRoomSummary()
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
timelineEventController.setTimeline(state.timeline, state.eventId)
|
||||
inviteView.visibility = View.GONE
|
||||
|
||||
val uid = session.sessionParams.credentials.userId
|
||||
|
@ -486,12 +493,7 @@ class RoomDetailFragment :
|
|||
state.asyncRoomSummary()?.let {
|
||||
roomToolbarTitleView.text = it.displayName
|
||||
AvatarRenderer.render(it, roomToolbarAvatarImageView)
|
||||
if (it.topic.isNotEmpty()) {
|
||||
roomToolbarSubtitleView.visibility = View.VISIBLE
|
||||
roomToolbarSubtitleView.text = it.topic
|
||||
} else {
|
||||
roomToolbarSubtitleView.visibility = View.GONE
|
||||
}
|
||||
roomToolbarSubtitleView.setTextOrHide(it.topic)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,9 +536,31 @@ class RoomDetailFragment :
|
|||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String) {
|
||||
// TODO Room can be the same
|
||||
permalinkHandler.launch(requireActivity(), url)
|
||||
override fun onUrlClicked(url: String): Boolean {
|
||||
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
||||
override fun navToRoom(roomId: String, eventId: String?): Boolean {
|
||||
// Same room?
|
||||
if (roomId == roomDetailArgs.roomId) {
|
||||
// Navigation to same room
|
||||
if (eventId == null) {
|
||||
showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room))
|
||||
} else {
|
||||
// Highlight and scroll to this event
|
||||
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId)))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Not handled
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onUrlLongClicked(url: String): Boolean {
|
||||
// Copy the url to the clipboard
|
||||
copyToClipboard(requireContext(), url)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onEventVisible(event: TimelineEvent) {
|
||||
|
@ -548,11 +572,13 @@ class RoomDetailFragment :
|
|||
}
|
||||
|
||||
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||
// TODO Use navigator
|
||||
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
|
||||
// TODO Use navigator
|
||||
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
@ -763,7 +789,7 @@ class RoomDetailFragment :
|
|||
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||
val snack = Snackbar.make(view!!, message, duration)
|
||||
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||
snack.show()
|
||||
|
|
|
@ -42,6 +42,7 @@ import io.reactivex.rxkotlin.subscribeBy
|
|||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.koin.android.ext.android.get
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -60,7 +61,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||
} else {
|
||||
TimelineDisplayableEvents.DISPLAYABLE_TYPES
|
||||
}
|
||||
private val timeline = room.createTimeline(eventId, allowedTypes)
|
||||
private var timeline = room.createTimeline(eventId, allowedTypes)
|
||||
|
||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||
|
||||
|
@ -98,6 +99,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
else -> Timber.e("Unhandled Action: $action")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,6 +131,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
|
||||
get() = _sendMessageResultLiveData
|
||||
|
||||
private val _navigateToEvent = MutableLiveData<LiveEvent<String>>()
|
||||
val navigateToEvent: LiveData<LiveEvent<String>>
|
||||
get() = _navigateToEvent
|
||||
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||
|
@ -403,6 +411,56 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
|
||||
val targetEventId = action.eventId
|
||||
|
||||
if (action.position != null) {
|
||||
// Event is already in RAM
|
||||
withState {
|
||||
if (it.eventId == targetEventId) {
|
||||
// ensure another click on the same permalink will also do a scroll
|
||||
setState {
|
||||
copy(
|
||||
eventId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
eventId = targetEventId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_navigateToEvent.postValue(LiveEvent(targetEventId))
|
||||
} else {
|
||||
// change timeline
|
||||
timeline.dispose()
|
||||
timeline = room.createTimeline(targetEventId, allowedTypes)
|
||||
timeline.start()
|
||||
|
||||
withState {
|
||||
if (it.eventId == targetEventId) {
|
||||
// ensure another click on the same permalink will also do a scroll
|
||||
setState {
|
||||
copy(
|
||||
eventId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
eventId = targetEventId,
|
||||
timeline = this@RoomDetailViewModel.timeline
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_navigateToEvent.postValue(LiveEvent(targetEventId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeEventDisplayedActions() {
|
||||
// We are buffering scroll events for one second
|
||||
|
|
|
@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
|
|||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
|
||||
|
@ -46,7 +45,6 @@ data class RoomDetailViewState(
|
|||
val timeline: Timeline? = null,
|
||||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized,
|
||||
val sendMode: SendMode = SendMode.REGULAR,
|
||||
val selectedEvent: TimelineEvent? = null
|
||||
) : MvRxState {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.riotredesign.features.home.room.detail
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager,
|
||||
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
|
||||
|
||||
private val scheduledEventId = AtomicReference<String?>()
|
||||
|
||||
override fun onChanged(position: Int, count: Int, tag: Any?) {
|
||||
val eventId = scheduledEventId.get() ?: return
|
||||
|
||||
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
|
||||
|
||||
if (positionToScroll != null) {
|
||||
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
|
||||
// Do not scroll it item is already visible
|
||||
if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
|
||||
// Note: Offset will be from the bottom, since the layoutManager is reversed
|
||||
layoutManager.scrollToPositionWithOffset(positionToScroll, 120)
|
||||
}
|
||||
scheduledEventId.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun scheduleScrollTo(eventId: String?) {
|
||||
scheduledEventId.set(eventId)
|
||||
}
|
||||
}
|
|
@ -46,9 +46,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||
|
||||
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback {
|
||||
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onUrlClicked(url: String)
|
||||
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||
|
@ -72,6 +71,11 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
fun onMemberNameClicked(informationData: MessageInformationData)
|
||||
}
|
||||
|
||||
interface UrlClickCallback {
|
||||
fun onUrlClicked(url: String): Boolean
|
||||
fun onUrlLongClicked(url: String): Boolean
|
||||
}
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<String>()
|
||||
private val mergeItemCollapseStates = HashMap<String, Boolean>()
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
|
@ -124,13 +128,30 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun setTimeline(timeline: Timeline?) {
|
||||
fun setTimeline(timeline: Timeline?, eventIdToHighlight: String?) {
|
||||
if (this.timeline != timeline) {
|
||||
this.timeline = timeline
|
||||
this.timeline?.listener = this
|
||||
|
||||
// Clear cache
|
||||
for (i in 0 until modelCache.size) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
}
|
||||
|
||||
if (this.eventIdToHighlight != eventIdToHighlight) {
|
||||
// Clear cache to force a refresh
|
||||
for (i in 0 until modelCache.size) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
this.eventIdToHighlight = eventIdToHighlight
|
||||
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
|
||||
private var eventIdToHighlight: String? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
timelineMediaSizeProvider.recyclerView = recyclerView
|
||||
|
@ -202,14 +223,14 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, callback).also {
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition)
|
||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
||||
|
||||
return CacheItemData(event.localId, eventModel, mergedHeaderModel, daySeparatorItem)
|
||||
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
|
||||
}
|
||||
|
||||
private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? {
|
||||
|
@ -221,6 +242,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
}
|
||||
}
|
||||
|
||||
// TODO Phase 3 Handle the case where the eventId we have to highlight is merged
|
||||
private fun buildMergedHeaderItem(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
items: List<TimelineEvent>,
|
||||
|
@ -270,10 +292,22 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
addIf(shouldAdd, this@TimelineEventController)
|
||||
}
|
||||
|
||||
fun searchPositionOfEvent(eventId: String): Int? {
|
||||
// Search in the cache
|
||||
modelCache.forEachIndexed { idx, cacheItemData ->
|
||||
if (cacheItemData?.eventId == eventId) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class CacheItemData(
|
||||
val localId: String,
|
||||
val eventId: String?,
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null
|
||||
|
|
|
@ -22,13 +22,15 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultIte
|
|||
|
||||
class DefaultItemFactory {
|
||||
|
||||
fun create(event: TimelineEvent, exception: Exception? = null): DefaultItem? {
|
||||
fun create(event: TimelineEvent, highlight: Boolean, exception: Exception? = null): DefaultItem? {
|
||||
val text = if (exception == null) {
|
||||
"${event.root.getClearType()} events are not yet handled"
|
||||
} else {
|
||||
"an exception occurred when rendering the event ${event.root.eventId}"
|
||||
}
|
||||
return DefaultItem_().text(text)
|
||||
return DefaultItem_()
|
||||
.text(text)
|
||||
.highlighted(highlight)
|
||||
}
|
||||
|
||||
}
|
|
@ -37,6 +37,7 @@ class EncryptedItemFactory(private val messageInformationDataFactory: MessageInf
|
|||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
||||
|
@ -62,7 +63,9 @@ class EncryptedItemFactory(private val messageInformationDataFactory: MessageInf
|
|||
return MessageTextItem_()
|
||||
.message(spannableStr)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.cellClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
callback?.onEncryptedMessageClicked(informationData, view)
|
||||
|
|
|
@ -33,6 +33,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
|||
class EncryptionItemFactory(private val stringProvider: StringProvider) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.BaseCallback?): NoticeItem? {
|
||||
val text = buildNoticeText(event.root, event.senderName) ?: return null
|
||||
val informationData = MessageInformationData(
|
||||
|
@ -46,6 +47,7 @@ class EncryptionItemFactory(private val stringProvider: StringProvider) {
|
|||
return NoticeItem_()
|
||||
.noticeText(text)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.baseCallback(callback)
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
@ -64,7 +65,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
|
||||
if (event.root.unsignedData?.redactedEvent != null) {
|
||||
//message is redacted
|
||||
return buildRedactedItem(informationData, callback)
|
||||
return buildRedactedItem(informationData, highlight, callback)
|
||||
}
|
||||
|
||||
val messageContent: MessageContent =
|
||||
|
@ -83,27 +84,31 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
||||
informationData,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback)
|
||||
is MessageTextContent -> buildTextMessageItem(event.sendState,
|
||||
messageContent,
|
||||
informationData,
|
||||
event.annotations?.editSummary,
|
||||
highlight,
|
||||
callback
|
||||
)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent, highlight)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||
return MessageFileItem_()
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.filename(messageContent.body)
|
||||
.iconRes(R.drawable.filetype_audio)
|
||||
|
@ -125,9 +130,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
|
||||
private fun buildFileMessageItem(messageContent: MessageFileContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||
return MessageFileItem_()
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.filename(messageContent.body)
|
||||
.reactionPillCallback(callback)
|
||||
|
@ -147,13 +154,16 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
}))
|
||||
}
|
||||
|
||||
private fun buildNotHandledMessageItem(messageContent: MessageContent): DefaultItem? {
|
||||
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
|
||||
val text = "${messageContent.type} message events are not yet handled"
|
||||
return DefaultItem_().text(text)
|
||||
return DefaultItem_()
|
||||
.text(text)
|
||||
.highlighted(highlight)
|
||||
}
|
||||
|
||||
private fun buildImageMessageItem(messageContent: MessageImageContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
|
||||
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
|
@ -170,6 +180,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
return MessageImageVideoItem_()
|
||||
.playable(messageContent.info?.mimeType == "image/gif")
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.mediaData(data)
|
||||
.reactionPillCallback(callback)
|
||||
|
@ -190,6 +201,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
|
||||
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
|
||||
|
||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||
|
@ -211,6 +223,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
return MessageImageVideoItem_()
|
||||
.playable(true)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.mediaData(thumbnailData)
|
||||
.reactionPillCallback(callback)
|
||||
|
@ -230,6 +243,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
messageContent: MessageTextContent,
|
||||
informationData: MessageInformationData,
|
||||
editSummary: EditAggregatedSummary?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val bodyToUse = messageContent.formattedBody?.let {
|
||||
|
@ -248,7 +262,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
}
|
||||
}
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.reactionPillCallback(callback)
|
||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||
//click on the text
|
||||
|
@ -298,6 +314,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
|
||||
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val message = messageContent.body.let {
|
||||
|
@ -311,8 +328,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
return MessageTextItem_()
|
||||
.message(message)
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.reactionPillCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||
.memberClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
|
@ -331,6 +350,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
||||
informationData: MessageInformationData,
|
||||
editSummary: EditAggregatedSummary?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
val message = messageContent.body.let {
|
||||
|
@ -347,8 +367,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
}
|
||||
}
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
.reactionPillCallback(callback)
|
||||
.urlClickCallback(callback)
|
||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||
.cellClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { view ->
|
||||
|
@ -361,9 +383,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
}
|
||||
|
||||
private fun buildRedactedItem(informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): RedactedMessageItem? {
|
||||
return RedactedMessageItem_()
|
||||
.informationData(informationData)
|
||||
.highlighted(highlight)
|
||||
.avatarCallback(callback)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
|||
class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val informationData = MessageInformationData(
|
||||
|
@ -41,6 +42,7 @@ class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {
|
|||
|
||||
return NoticeItem_()
|
||||
.noticeText(formattedText)
|
||||
.highlighted(highlight)
|
||||
.informationData(informationData)
|
||||
.baseCallback(callback)
|
||||
}
|
||||
|
|
|
@ -37,11 +37,13 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
|||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
eventIdToHighlight: String?,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||
val highlight = event.root.eventId == eventIdToHighlight
|
||||
|
||||
val computedModel = try {
|
||||
when (event.root.getClearType()) {
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
// State and call
|
||||
EventType.STATE_ROOM_NAME,
|
||||
EventType.STATE_ROOM_TOPIC,
|
||||
|
@ -49,16 +51,16 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
|||
EventType.STATE_HISTORY_VISIBILITY,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback)
|
||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
|
||||
|
||||
// Crypto
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, callback)
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, callback)
|
||||
EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
|
||||
// Unhandled event types (yet)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
|
||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight)
|
||||
|
||||
else -> {
|
||||
//These are just for debug to display hidden event, they should be filtered out in normal mode
|
||||
|
@ -77,6 +79,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
|||
MessageTextItem_()
|
||||
.informationData(informationData)
|
||||
.message("{ \"type\": ${event.root.type} }")
|
||||
.highlighted(highlight)
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||
?: false
|
||||
|
@ -89,7 +92,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "failed to create message item")
|
||||
defaultItemFactory.create(event, e)
|
||||
defaultItemFactory.create(event, highlight, e)
|
||||
}
|
||||
return (computedModel ?: EmptyItem_())
|
||||
}
|
||||
|
|
|
@ -19,20 +19,28 @@ import android.view.View
|
|||
import android.view.ViewStub
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.constraintlayout.widget.Guideline
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.core.platform.CheckableView
|
||||
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||
|
||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
||||
|
||||
var avatarStyle: AvatarStyle = Companion.AvatarStyle.SMALL
|
||||
var avatarStyle: AvatarStyle = AvatarStyle.SMALL
|
||||
|
||||
// To use for instance when opening a permalink with an eventId
|
||||
@EpoxyAttribute
|
||||
var highlighted: Boolean = false
|
||||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
//optimize?
|
||||
val px = dpToPx(avatarStyle.avatarSizeDP, holder.view.context)
|
||||
val px = dpToPx(avatarStyle.avatarSizeDP + 8, holder.view.context)
|
||||
holder.leftGuideline.setGuidelineBegin(px)
|
||||
|
||||
holder.checkableBackground.isChecked = highlighted
|
||||
}
|
||||
|
||||
|
||||
|
@ -46,6 +54,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
abstract class BaseHolder : VectorEpoxyHolder() {
|
||||
|
||||
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
|
||||
@IdRes
|
||||
abstract fun getStubId(): Int
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.airbnb.epoxy.EpoxyAttribute
|
|||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.utils.containsOnlyEmojis
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.html.PillImageSpan
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
@ -38,15 +39,18 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||
var message: CharSequence? = null
|
||||
@EpoxyAttribute
|
||||
override lateinit var informationData: MessageInformationData
|
||||
@EpoxyAttribute
|
||||
var urlClickCallback: TimelineEventController.UrlClickCallback? = null
|
||||
|
||||
val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
|
||||
it.setOnLinkClickListener { textView, url ->
|
||||
//Return false to let android manage the click on the link
|
||||
false
|
||||
// TODO Move this instantiation somewhere else?
|
||||
private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
|
||||
it.setOnLinkClickListener { _, url ->
|
||||
//Return false to let android manage the click on the link, or true if the link is handled by the application
|
||||
urlClickCallback?.onUrlClicked(url) == true
|
||||
}
|
||||
it.setOnLinkLongClickListener { textView, url ->
|
||||
it.setOnLinkLongClickListener { _, url ->
|
||||
//Long clicks are handled by parent, return true to block android to do something with url
|
||||
true
|
||||
urlClickCallback?.onUrlLongClicked(url) == true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ package im.vector.riotredesign.features.navigation
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
||||
import im.vector.riotredesign.core.utils.toast
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
import im.vector.riotredesign.features.debug.DebugMenuActivity
|
||||
|
@ -38,6 +41,14 @@ class DefaultNavigator : Navigator {
|
|||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) {
|
||||
if (context is VectorBaseActivity) {
|
||||
context.notImplemented("Open not joined room")
|
||||
} else {
|
||||
context.toast(R.string.not_implemented)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
|
||||
val intent = RoomPreviewActivity.getIntent(context, publicRoom)
|
||||
context.startActivity(intent)
|
||||
|
|
|
@ -23,6 +23,8 @@ interface Navigator {
|
|||
|
||||
fun openRoom(context: Context, roomId: String, eventId: String? = null)
|
||||
|
||||
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null)
|
||||
|
||||
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
|
||||
|
||||
fun openRoomDirectory(context: Context)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:state_checked="true">
|
||||
|
||||
<layer-list>
|
||||
|
||||
<!-- Draw the BG. -->
|
||||
<item android:left="6dp" android:right="2dp">
|
||||
<shape>
|
||||
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp" />
|
||||
<!-- TODO Handle drawable for dark themes -->
|
||||
<solid android:color="@color/riotx_header_panel_background_light" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:gravity="start" android:left="2dp">
|
||||
<shape>
|
||||
<size android:width="4dp" />
|
||||
<corners android:bottomLeftRadius="40dp" android:topLeftRadius="40dp" />
|
||||
<solid android:color="@color/riotx_accent" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
|
||||
</item>
|
||||
|
||||
<item android:state_checked="false">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
|
||||
</selector>
|
|
@ -5,14 +5,24 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:addStatesFromChildren="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp">
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<im.vector.riotredesign.core.platform.CheckableView
|
||||
android:id="@+id/messageSelectedBackground"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/highligthed_message_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageAvatarImageView"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -23,7 +33,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:layout_constraintGuide_begin="44dp" />
|
||||
tools:layout_constraintGuide_begin="52dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageMemberNameView"
|
||||
|
@ -54,6 +64,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/messageMemberNameView"
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
<?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"
|
||||
android:addStatesFromChildren="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp">
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<im.vector.riotredesign.core.platform.CheckableView
|
||||
android:id="@+id/messageSelectedBackground"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/highligthed_message_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/messageStartGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:layout_constraintGuide_begin="44dp" />
|
||||
tools:layout_constraintGuide_begin="52dp" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentNoticeStub"
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
|
||||
<string name="settings_sdk_version">Matrix SDK Version</string>
|
||||
<string name="settings_other_third_party_notices">Other third party notices</string>
|
||||
<string name="navigate_to_room_when_already_in_the_room">You are already viewing this room!</string>
|
||||
|
||||
</resources>
|
|
@ -266,6 +266,8 @@
|
|||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginStart">8dp</item>
|
||||
<item name="android:layout_marginLeft">8dp</item>
|
||||
<item name="android:layout_marginEnd">8dp</item>
|
||||
<item name="android:layout_marginRight">8dp</item>
|
||||
<item name="android:layout_marginBottom">4dp</item>
|
||||
<item name="android:layout_marginTop">4dp</item>
|
||||
<item name="layout_constraintBottom_toBottomOf">parent</item>
|
||||
|
|
Loading…
Reference in a new issue