diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt index 17ad30c4..15d56eac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt index 19d37cf4..71fd16e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 2c2530bb..5f387926 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineData.kt deleted file mode 100644 index eab4cc45..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineData.kt +++ /dev/null @@ -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, - - /** - * 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 -) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index c05bd138..3341d87e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -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( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEventInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEventInterceptor.kt deleted file mode 100644 index 3a4ff224..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEventInterceptor.kt +++ /dev/null @@ -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) - -} - diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/CheckableView.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/CheckableView.kt new file mode 100644 index 00000000..a590dda5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/CheckableView.kt @@ -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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index bbe85a4e..9d12a1cf 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -51,7 +51,7 @@ class HomeModule { } scope(HOME_SCOPE) { - PermalinkHandler(get()) + PermalinkHandler(get(), get()) } // Fragment scopes diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/PermalinkHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/home/PermalinkHandler.kt index 6bc86972..537f7f93 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/PermalinkHandler.kt @@ -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 } } } -} \ No newline at end of file + + /** + * 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 + +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index ddee224b..03763001 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 985d5f95..525371a2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index eee29328..0a384f3c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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 { @@ -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> get() = _sendMessageResultLiveData + private val _navigateToEvent = MutableLiveData>() + val navigateToEvent: LiveData> + 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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index 6151b425..927bbba1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -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 = Uninitialized, val asyncRoomSummary: Async = Uninitialized, - val asyncTimelineData: Async = Uninitialized, val sendMode: SendMode = SendMode.REGULAR, val selectedEvent: TimelineEvent? = null ) : MvRxState { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnHighlightedEventCallback.kt new file mode 100644 index 00000000..e9950fbb --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -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() + + 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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 0720fd23..311355ce 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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() private val mergeItemCollapseStates = HashMap() private val modelCache = arrayListOf() @@ -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, @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index c44af0ea..8c019a09 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -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) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 7f2aca51..258e11fc 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index acd5162d..32b7b295 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index cc733382..967b9b29 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 9a87b80f..6e38ceb0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index f3a53ef7..63d71898 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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_()) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/BaseEventItem.kt index c72fe659..45da881f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -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 : VectorEpoxyModel() { - 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 : VectorEpoxyModel abstract class BaseHolder : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) + val checkableBackground by bind(R.id.messageSelectedBackground) @IdRes abstract fun getStubId(): Int diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt index 12815241..4c26d55b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -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() { 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 } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt index da796dda..d1ca13d2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt index d04a9b7a..9908f246 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt @@ -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) diff --git a/vector/src/main/res/drawable/highligthed_message_background.xml b/vector/src/main/res/drawable/highligthed_message_background.xml new file mode 100644 index 00000000..82e84589 --- /dev/null +++ b/vector/src/main/res/drawable/highligthed_message_background.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 6de6d85f..3acf9a21 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -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"> + + + tools:layout_constraintGuide_begin="52dp" /> + android:background="?attr/selectableItemBackground"> + + + tools:layout_constraintGuide_begin="52dp" /> Matrix SDK Version Other third party notices + You are already viewing this room! \ No newline at end of file diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index a6780efd..83004b0d 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -266,6 +266,8 @@ wrap_content 8dp 8dp + 8dp + 8dp 4dp 4dp parent