From 73b55fd975e4bae7580716f9013f2c2aa0d38d8f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jun 2019 10:47:55 +0200 Subject: [PATCH 1/8] Group navigation cleanup --- .../im/vector/riotredesign/features/home/HomeNavigator.kt | 4 ++-- .../riotredesign/features/home/group/GroupListFragment.kt | 4 ++-- .../features/home/group/GroupListViewModel.kt | 4 +++- vector/src/main/res/layout/fragment_group_list.xml | 8 ++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index e46fd7f59e..5d6216041d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -34,9 +34,10 @@ class HomeNavigator { fun openSelectedGroup(groupSummary: GroupSummary) { Timber.v("Open selected group ${groupSummary.groupId}") activity?.let { + it.drawerLayout?.closeDrawer(GravityCompat.START) + val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl) val homeDetailFragment = HomeDetailFragment.newInstance(args) - it.drawerLayout?.closeDrawer(GravityCompat.START) it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) } } @@ -47,7 +48,6 @@ class HomeNavigator { Timber.v("Open room detail $roomId - $eventId") activity?.let { //TODO enable eventId permalink. It doesn't work enough at the moment. - it.drawerLayout?.closeDrawer(GravityCompat.START) navigator.openRoom(roomId, it) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt index ba65cf72c5..8709cd35e2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt @@ -46,8 +46,8 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) groupController.callback = this - stateView.contentView = epoxyRecyclerView - epoxyRecyclerView.setController(groupController) + stateView.contentView = groupListEpoxyRecyclerView + groupListEpoxyRecyclerView.setController(groupController) viewModel.subscribe { renderState(it) } viewModel.openGroupLiveData.observeEvent(this) { homeNavigator.openSelectedGroup(it) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt index c06cbe5e4a..4b15ac71a0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt @@ -84,7 +84,8 @@ class GroupListViewModel(initialState: GroupListViewState, private fun observeGroupSummaries() { session - .rx().liveGroupSummaries() + .rx() + .liveGroupSummaries() .map { val myUser = session.getUser(session.sessionParams.credentials.userId) val allCommunityGroup = GroupSummary( @@ -94,6 +95,7 @@ class GroupListViewModel(initialState: GroupListViewState, listOf(allCommunityGroup) + it } .execute { async -> + // TODO Phase2 Handle the case where the selected group is deleted on another client val newSelectedGroup = selectedGroup ?: async()?.firstOrNull() copy(asyncGroups = async, selectedGroup = newSelectedGroup) } diff --git a/vector/src/main/res/layout/fragment_group_list.xml b/vector/src/main/res/layout/fragment_group_list.xml index 810fe3e47b..5548eacc98 100644 --- a/vector/src/main/res/layout/fragment_group_list.xml +++ b/vector/src/main/res/layout/fragment_group_list.xml @@ -1,14 +1,14 @@ - - + android:layout_height="match_parent" + tools:listitem="@layout/item_group" /> From 90f420b2873b3a1c2307edffc9bd4dfc3b745d2a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jun 2019 11:10:36 +0200 Subject: [PATCH 2/8] Cleanup PermalinkHandler and Navigation --- .../riotredesign/features/home/HomeModule.kt | 2 +- .../features/home/HomeNavigator.kt | 38 +------------------ ...ermalinkHandler.kt => PermalinkHandler.kt} | 33 ++++++++-------- .../home/room/detail/RoomDetailFragment.kt | 7 ++-- .../home/room/list/RoomListFragment.kt | 2 +- .../features/navigation/DefaultNavigator.kt | 13 ++++++- .../features/navigation/Navigator.kt | 6 ++- .../roomdirectory/PublicRoomsFragment.kt | 2 +- .../createroom/CreateRoomFragment.kt | 2 +- .../RoomPreviewNoPreviewFragment.kt | 2 +- 10 files changed, 42 insertions(+), 65 deletions(-) rename vector/src/main/java/im/vector/riotredesign/features/home/{HomePermalinkHandler.kt => PermalinkHandler.kt} (54%) 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 7f85269ab8..bbe85a4e3b 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) { - HomePermalinkHandler(get(), get()) + PermalinkHandler(get()) } // Fragment scopes diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index 5d6216041d..729b8722a7 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -17,11 +17,9 @@ package im.vector.riotredesign.features.home import androidx.core.view.GravityCompat -import androidx.fragment.app.FragmentManager import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotredesign.R import im.vector.riotredesign.core.extensions.replaceFragment -import im.vector.riotredesign.features.navigation.Navigator import kotlinx.android.synthetic.main.activity_home.* import timber.log.Timber @@ -29,8 +27,6 @@ class HomeNavigator { var activity: HomeActivity? = null - private var rootRoomId: String? = null - fun openSelectedGroup(groupSummary: GroupSummary) { Timber.v("Open selected group ${groupSummary.groupId}") activity?.let { @@ -41,36 +37,4 @@ class HomeNavigator { it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) } } - - fun openRoomDetail(roomId: String, - eventId: String?, - navigator: Navigator) { - Timber.v("Open room detail $roomId - $eventId") - activity?.let { - //TODO enable eventId permalink. It doesn't work enough at the moment. - navigator.openRoom(roomId, it) - } - } - - fun openGroupDetail(groupId: String) { - Timber.v("Open group detail $groupId") - } - - fun openUserDetail(userId: String) { - Timber.v("Open user detail $userId") - } - - // Private Methods ***************************************************************************** - - private fun clearBackStack(fragmentManager: FragmentManager) { - if (fragmentManager.backStackEntryCount > 0) { - val first = fragmentManager.getBackStackEntryAt(0) - fragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE) - } - } - - private fun isRoot(roomId: String): Boolean { - return rootRoomId == roomId - } - -} \ No newline at end of file +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/home/PermalinkHandler.kt similarity index 54% rename from vector/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt rename to vector/src/main/java/im/vector/riotredesign/features/home/PermalinkHandler.kt index dc387aa5b0..6bc8697263 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/PermalinkHandler.kt @@ -16,41 +16,40 @@ package im.vector.riotredesign.features.home +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.riotredesign.features.navigation.Navigator -class HomePermalinkHandler(private val homeNavigator: HomeNavigator, - private val navigator: Navigator) { +class PermalinkHandler(private val navigator: Navigator) { - fun launch(deepLink: String?) { + fun launch(context: Context, deepLink: String?) { val uri = deepLink?.let { Uri.parse(it) } - launch(uri) + launch(context, uri) } - fun launch(deepLink: Uri?) { + fun launch(context: Context, deepLink: Uri?) { if (deepLink == null) { return } - val permalinkData = PermalinkParser.parse(deepLink) - when (permalinkData) { - is PermalinkData.EventLink -> { - homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, navigator) + when (val permalinkData = PermalinkParser.parse(deepLink)) { + is PermalinkData.EventLink -> { + navigator.openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId) } - is PermalinkData.RoomLink -> { - homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, null, navigator) + is PermalinkData.RoomLink -> { + navigator.openRoom(context, permalinkData.roomIdOrAlias) } - is PermalinkData.GroupLink -> { - homeNavigator.openGroupDetail(permalinkData.groupId) + is PermalinkData.GroupLink -> { + navigator.openGroupDetail(permalinkData.groupId, context) } - is PermalinkData.UserLink -> { - homeNavigator.openUserDetail(permalinkData.userId) + is PermalinkData.UserLink -> { + navigator.openUserDetail(permalinkData.userId, context) } is PermalinkData.FallbackLink -> { - + openUrlInExternalBrowser(context, permalinkData.uri) } } } - } \ No newline at end of file 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 c8030c25d6..985d5f95c9 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 @@ -74,7 +74,7 @@ import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresent 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.HomePermalinkHandler +import im.vector.riotredesign.features.home.PermalinkHandler import im.vector.riotredesign.features.home.getColorFromUserId import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView @@ -167,7 +167,7 @@ class RoomDetailFragment : private val commandAutocompletePolicy = CommandAutocompletePolicy() private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) } private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) } - private val homePermalinkHandler: HomePermalinkHandler by inject() + private val permalinkHandler: PermalinkHandler by inject() private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback @@ -535,7 +535,8 @@ class RoomDetailFragment : // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String) { - homePermalinkHandler.launch(url) + // TODO Room can be the same + permalinkHandler.launch(requireActivity(), url) } override fun onEventVisible(event: TimelineEvent) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index e7b8b8378a..957a0d0a81 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -72,7 +72,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O setupRecyclerView() roomListViewModel.subscribe { renderState(it) } roomListViewModel.openRoomLiveData.observeEvent(this) { - navigator.openRoom(it, requireActivity()) + navigator.openRoom(requireActivity(), it) } createChatFabMenu.listener = this 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 3dd5c6d9f5..da796ddaf1 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 @@ -27,12 +27,13 @@ import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotredesign.features.settings.VectorSettingsActivity +import timber.log.Timber class DefaultNavigator : Navigator { - override fun openRoom(roomId: String, context: Context) { - val args = RoomDetailArgs(roomId) + override fun openRoom(context: Context, roomId: String, eventId: String?) { + val args = RoomDetailArgs(roomId, eventId) val intent = RoomDetailActivity.newIntent(context, args) context.startActivity(intent) } @@ -63,4 +64,12 @@ class DefaultNavigator : Navigator { override fun openKeysBackupManager(context: Context) { context.startActivity(KeysBackupManageActivity.intent(context)) } + + override fun openGroupDetail(groupId: String, context: Context) { + Timber.v("Open group detail $groupId") + } + + override fun openUserDetail(userId: String, context: Context) { + Timber.v("Open user detail $userId") + } } \ No newline at end of file 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 3754529ffb..d04a9b7a4c 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 @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom interface Navigator { - fun openRoom(roomId: String, context: Context) + fun openRoom(context: Context, roomId: String, eventId: String? = null) fun openRoomPreview(publicRoom: PublicRoom, context: Context) @@ -34,4 +34,8 @@ interface Navigator { fun openKeysBackupSetup(context: Context, showManualExport: Boolean) fun openKeysBackupManager(context: Context) + + fun openGroupDetail(groupId: String, context: Context) + + fun openUserDetail(userId: String, context: Context) } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt index 1f680b9ea0..ce6e24bbc9 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt @@ -122,7 +122,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback when (joinState) { JoinState.JOINED -> { - navigator.openRoom(publicRoom.roomId, requireActivity()) + navigator.openRoom(requireActivity(), publicRoom.roomId) } JoinState.NOT_JOINED, JoinState.JOINING_ERROR -> { diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/createroom/CreateRoomFragment.kt index 96e7c60cb1..07c6c0e3ea 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -100,7 +100,7 @@ class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener { val async = state.asyncCreateRoomRequest if (async is Success) { // Navigate to freshly created room - navigator.openRoom(async(), requireActivity()) + navigator.openRoom(requireActivity(), async()) navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Close) } else { diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 3f389a4609..1fdd5e0966 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -108,7 +108,7 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() { // Quit this screen requireActivity().finish() // Open room - navigator.openRoom(roomPreviewData.roomId, requireActivity()) + navigator.openRoom(requireActivity(), roomPreviewData.roomId) } } } \ No newline at end of file From b1e009f8b457326a73f4c177f59144380202b21d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jun 2019 13:51:52 +0200 Subject: [PATCH 3/8] Handle eventId v4 (https://matrix.org/docs/spec/rooms/v4#event-ids) --- .../java/im/vector/matrix/android/api/MatrixPatterns.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt index 19dc37e615..eaaeb73075 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt @@ -47,6 +47,10 @@ object MatrixPatterns { private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+" private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V3_REGEX, Pattern.CASE_INSENSITIVE) + // Ref: https://matrix.org/docs/spec/rooms/v4#event-ids + private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V4_REGEX, Pattern.CASE_INSENSITIVE) + // regex pattern to find group ids in a string. private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX" private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) @@ -120,7 +124,9 @@ object MatrixPatterns { */ fun isEventId(str: String?): Boolean { return str != null - && (PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches() || PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3.matcher(str).matches()) + && (PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches() + || PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3.matcher(str).matches() + || PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4.matcher(str).matches()) } /** From 76ade2957e0c89430d700eaa6892f63f8cddf5fe Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jun 2019 16:27:43 +0200 Subject: [PATCH 4/8] Handle permalink click --- .../api/permalinks/PermalinkFactory.kt | 2 +- .../android/api/permalinks/PermalinkParser.kt | 10 +++- .../api/session/room/timeline/Timeline.kt | 2 +- .../api/session/room/timeline/TimelineData.kt | 41 ------------- .../session/room/timeline/TimelineEvent.kt | 2 +- .../room/timeline/TimelineEventInterceptor.kt | 27 --------- .../core/platform/CheckableView.kt | 60 +++++++++++++++++++ .../riotredesign/features/home/HomeModule.kt | 2 +- .../features/home/PermalinkHandler.kt | 54 +++++++++++++---- .../home/room/detail/RoomDetailActions.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 56 ++++++++++++----- .../home/room/detail/RoomDetailViewModel.kt | 60 ++++++++++++++++++- .../home/room/detail/RoomDetailViewState.kt | 2 - .../ScrollOnHighlightedEventCallback.kt | 50 ++++++++++++++++ .../timeline/TimelineEventController.kt | 44 ++++++++++++-- .../timeline/factory/DefaultItemFactory.kt | 6 +- .../timeline/factory/EncryptedItemFactory.kt | 3 + .../timeline/factory/EncryptionItemFactory.kt | 2 + .../timeline/factory/MessageItemFactory.kt | 42 ++++++++++--- .../timeline/factory/NoticeItemFactory.kt | 2 + .../timeline/factory/TimelineItemFactory.kt | 15 +++-- .../detail/timeline/item/BaseEventItem.kt | 13 +++- .../detail/timeline/item/MessageTextItem.kt | 16 +++-- .../features/navigation/DefaultNavigator.kt | 11 ++++ .../features/navigation/Navigator.kt | 2 + .../highligthed_message_background.xml | 36 +++++++++++ .../res/layout/item_timeline_event_base.xml | 20 +++++-- .../item_timeline_event_base_noinfo.xml | 17 ++++-- vector/src/main/res/values/strings_riotX.xml | 1 + vector/src/main/res/values/styles_riot.xml | 2 + 30 files changed, 461 insertions(+), 140 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineData.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEventInterceptor.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/core/platform/CheckableView.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnHighlightedEventCallback.kt create mode 100644 vector/src/main/res/drawable/highligthed_message_background.xml 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 17ad30c4da..15d56eacb3 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 19d37cf438..71fd16e778 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 2c2530bb0a..5f387926ed 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 eab4cc455f..0000000000 --- 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 c05bd138e3..3341d87ebd 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 3a4ff2241f..0000000000 --- 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 0000000000..a590dda5d6 --- /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 bbe85a4e3b..9d12a1cf4a 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 6bc8697263..537f7f938e 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 ddee224bd1..037630014c 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 985d5f95c9..525371a281 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 eee29328c8..0a384f3cea 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 6151b42571..927bbba11b 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 0000000000..e9950fbb36 --- /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 0720fd234b..311355cec6 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 c44af0ea86..8c019a09b7 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 7f2aca512e..258e11fcbc 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 acd5162d3b..32b7b29555 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 cc7333829b..967b9b2979 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 9a87b80f42..6e38ceb064 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 f3a53ef72c..63d71898a6 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 c72fe65964..45da881fe1 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 128152417c..4c26d55b66 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 da796ddaf1..d1ca13d2cf 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 d04a9b7a4c..9908f246ac 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 0000000000..82e845899d --- /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 6de6d85fbe..3acf9a2107 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 a6780efd25..83004b0de2 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 From 3e9750322022a70886418bfa6b3e11ce60717da1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jun 2019 16:42:22 +0200 Subject: [PATCH 5/8] Avoid erasing all cache --- .../home/room/detail/timeline/TimelineEventController.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 311355cec6..a93a948fe2 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 @@ -142,7 +142,10 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, if (this.eventIdToHighlight != eventIdToHighlight) { // Clear cache to force a refresh for (i in 0 until modelCache.size) { - modelCache[i] = null + if (modelCache[i]?.eventId == eventIdToHighlight + || modelCache[i]?.eventId == this.eventIdToHighlight) { + modelCache[i] = null + } } this.eventIdToHighlight = eventIdToHighlight From 401f878a9cb53e98c120db2c3b802bde924054ba Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jun 2019 17:03:14 +0200 Subject: [PATCH 6/8] Fix ConcurrentModificationException --- .../timeline/TimelineEventController.kt | 116 ++++++++++-------- 1 file changed, 63 insertions(+), 53 deletions(-) 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 a93a948fe2..4c49ec2d60 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 @@ -88,39 +88,43 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val listUpdateCallback = object : ListUpdateCallback { - @Synchronized override fun onChanged(position: Int, count: Int, payload: Any?) { - assertUpdateCallbacksAllowed() - (position until (position + count)).forEach { - modelCache[it] = null + synchronized(modelCache) { + assertUpdateCallbacksAllowed() + (position until (position + count)).forEach { + modelCache[it] = null + } + requestModelBuild() } - requestModelBuild() } - @Synchronized override fun onMoved(fromPosition: Int, toPosition: Int) { - assertUpdateCallbacksAllowed() - val model = modelCache.removeAt(fromPosition) - modelCache.add(toPosition, model) - requestModelBuild() + synchronized(modelCache) { + assertUpdateCallbacksAllowed() + val model = modelCache.removeAt(fromPosition) + modelCache.add(toPosition, model) + requestModelBuild() + } } - @Synchronized override fun onInserted(position: Int, count: Int) { - assertUpdateCallbacksAllowed() - (0 until count).forEach { - modelCache.add(position, null) + synchronized(modelCache) { + assertUpdateCallbacksAllowed() + (0 until count).forEach { + modelCache.add(position, null) + } + requestModelBuild() } - requestModelBuild() } - @Synchronized override fun onRemoved(position: Int, count: Int) { - assertUpdateCallbacksAllowed() - (0 until count).forEach { - modelCache.removeAt(position) + synchronized(modelCache) { + assertUpdateCallbacksAllowed() + (0 until count).forEach { + modelCache.removeAt(position) + } + requestModelBuild() } - requestModelBuild() } } @@ -134,17 +138,21 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, this.timeline?.listener = this // Clear cache - for (i in 0 until modelCache.size) { - modelCache[i] = null + synchronized(modelCache) { + 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) { - if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { - modelCache[i] = null + synchronized(modelCache) { + for (i in 0 until modelCache.size) { + if (modelCache[i]?.eventId == eventIdToHighlight + || modelCache[i]?.eventId == this.eventIdToHighlight) { + modelCache[i] = null + } } } this.eventIdToHighlight = eventIdToHighlight @@ -194,28 +202,29 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, require(inSubmitList || Looper.myLooper() == backgroundHandler.looper) } - @Synchronized private fun getModels(): List> { - (0 until modelCache.size).forEach { position -> - // Should be build if not cached or if cached but contains mergedHeader or formattedDay - // We then are sure we always have items up to date. - if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { - modelCache[position] = buildItemModels(position, currentSnapshot) - } - } - return modelCache - .map { - val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) { - null - } else { - it.eventModel - } - listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + synchronized(modelCache) { + (0 until modelCache.size).forEach { position -> + // Should be build if not cached or if cached but contains mergedHeader or formattedDay + // We then are sure we always have items up to date. + if (modelCache[position] == null + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { + modelCache[position] = buildItemModels(position, currentSnapshot) } - .flatten() - .filterNotNull() + } + return modelCache + .map { + val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) { + null + } else { + it.eventModel + } + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + } + .flatten() + .filterNotNull() + } } @@ -296,16 +305,17 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, } fun searchPositionOfEvent(eventId: String): Int? { - // Search in the cache - modelCache.forEachIndexed { idx, cacheItemData -> - if (cacheItemData?.eventId == eventId) { - return idx + synchronized(modelCache) { + // Search in the cache + modelCache.forEachIndexed { idx, cacheItemData -> + if (cacheItemData?.eventId == eventId) { + return idx + } } + + return null } - - return null } - } private data class CacheItemData( From 625242a3d9d6c74e51ae313c8ae97c412e8f3c8e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jun 2019 17:04:52 +0200 Subject: [PATCH 7/8] handle all themes --- .../highlighted_message_background_black.xml | 35 +++++++++++++++++++ .../highlighted_message_background_dark.xml | 35 +++++++++++++++++++ ... highlighted_message_background_light.xml} | 1 - .../res/layout/item_timeline_event_base.xml | 2 +- .../item_timeline_event_base_noinfo.xml | 2 +- vector/src/main/res/values/attrs.xml | 2 ++ vector/src/main/res/values/theme_black.xml | 3 ++ vector/src/main/res/values/theme_dark.xml | 3 ++ vector/src/main/res/values/theme_light.xml | 6 ++-- vector/src/main/res/values/theme_status.xml | 3 +- 10 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/res/drawable/highlighted_message_background_black.xml create mode 100644 vector/src/main/res/drawable/highlighted_message_background_dark.xml rename vector/src/main/res/drawable/{highligthed_message_background.xml => highlighted_message_background_light.xml} (93%) diff --git a/vector/src/main/res/drawable/highlighted_message_background_black.xml b/vector/src/main/res/drawable/highlighted_message_background_black.xml new file mode 100644 index 0000000000..ebb4c81dd9 --- /dev/null +++ b/vector/src/main/res/drawable/highlighted_message_background_black.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/highlighted_message_background_dark.xml b/vector/src/main/res/drawable/highlighted_message_background_dark.xml new file mode 100644 index 0000000000..4448f01d0e --- /dev/null +++ b/vector/src/main/res/drawable/highlighted_message_background_dark.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/highligthed_message_background.xml b/vector/src/main/res/drawable/highlighted_message_background_light.xml similarity index 93% rename from vector/src/main/res/drawable/highligthed_message_background.xml rename to vector/src/main/res/drawable/highlighted_message_background_light.xml index 82e845899d..7b53b3ffe3 100644 --- a/vector/src/main/res/drawable/highligthed_message_background.xml +++ b/vector/src/main/res/drawable/highlighted_message_background_light.xml @@ -9,7 +9,6 @@ - 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 3acf9a2107..9fda155fd9 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -11,7 +11,7 @@ android:id="@+id/messageSelectedBackground" android:layout_width="0dp" android:layout_height="0dp" - android:background="@drawable/highligthed_message_background" + android:background="?riotx_highlighted_message_background" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index aa183e49ec..ebd6e31773 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -11,7 +11,7 @@ android:id="@+id/messageSelectedBackground" android:layout_width="0dp" android:layout_height="0dp" - android:background="@drawable/highligthed_message_background" + android:background="?riotx_highlighted_message_background" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 04c959ee21..368c03c0c1 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -92,5 +92,7 @@ + + diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 83b27bb486..0b2d213586 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -31,6 +31,9 @@ @color/riotx_fab_label_color_black @color/riotx_touch_guard_bg_black + + @drawable/highlighted_message_background_black + @color/riotx_accent diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index d8300a68db..986ed33d5d 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -30,6 +30,9 @@ @color/riotx_touch_guard_bg_dark @color/riotx_keys_backup_banner_accent_color_dark + + @drawable/highlighted_message_background_dark + @color/riotx_accent @color/primary_color_dark_light diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 269a51bb95..ad487d1d61 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -30,6 +30,9 @@ @color/riotx_touch_guard_bg_light @color/riotx_keys_backup_banner_accent_color_light + + @drawable/highlighted_message_background_light + @color/riotx_accent @@ -153,8 +156,7 @@ @drawable/vector_tabbar_background_light @drawable/pill_background_user_id_light - @drawable/pill_background_room_alias_light - + @drawable/pill_background_room_alias_light @color/riot_primary_text_color_light @android:color/white diff --git a/vector/src/main/res/values/theme_status.xml b/vector/src/main/res/values/theme_status.xml index a1f4bf4aa4..6ad65f6a1f 100644 --- a/vector/src/main/res/values/theme_status.xml +++ b/vector/src/main/res/values/theme_status.xml @@ -111,8 +111,7 @@ @drawable/vector_tabbar_background_status @drawable/pill_background_user_id_status - @drawable/pill_background_room_alias_status - + @drawable/pill_background_room_alias_status @color/riot_primary_text_color_status @android:color/white From 27417805533a0d2bef0c6d8afcf1d3a7ce77c231 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Jun 2019 17:05:15 +0200 Subject: [PATCH 8/8] Change scope of PermalinkHandler --- .../im/vector/riotredesign/features/home/HomeModule.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 9d12a1cf4a..044bf88120 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 @@ -50,10 +50,6 @@ class HomeModule { HomeNavigator() } - scope(HOME_SCOPE) { - PermalinkHandler(get(), get()) - } - // Fragment scopes factory { @@ -98,6 +94,10 @@ class HomeModule { GroupSummaryController() } + scope(ROOM_DETAIL_SCOPE) { + PermalinkHandler(get(), get()) + } + scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) -> val commandController = AutocompleteCommandController(get()) AutocompleteCommandPresenter(fragment.requireContext(), commandController)