Merge pull request #200 from vector-im/feature/permalinks

Handle permalink with the new navigation UX - WIP
This commit is contained in:
Benoit Marty 2019-06-25 09:34:58 +02:00 committed by GitHub
commit 104ffc930d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 683 additions and 286 deletions

View file

@ -47,6 +47,10 @@ object MatrixPatterns {
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+" 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) 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. // regex pattern to find group ids in a string.
private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX" 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) 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 { fun isEventId(str: String?): Boolean {
return str != null 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())
} }
/** /**

View file

@ -24,7 +24,7 @@ import im.vector.matrix.android.api.session.events.model.Event
*/ */
object PermalinkFactory { 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. * Creates a permalink for an event.

View file

@ -36,12 +36,20 @@ object PermalinkParser {
* Turns an uri to a [PermalinkData] * Turns an uri to a [PermalinkData]
*/ */
fun parse(uri: Uri): PermalinkData { fun parse(uri: Uri): PermalinkData {
if (!uri.toString().startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) {
return PermalinkData.FallbackLink(uri)
}
val fragment = uri.fragment val fragment = uri.fragment
if (fragment.isNullOrEmpty()) { if (fragment.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri) 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 // we are limiting to 2 params
val params = fragment val params = safeFragment
.split(MatrixPatterns.SEP_REGEX.toRegex()) .split(MatrixPatterns.SEP_REGEX.toRegex())
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.take(2) .take(2)

View file

@ -32,7 +32,7 @@ package im.vector.matrix.android.api.session.room.timeline
*/ */
interface 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 * This should be called before any other method after creating the timeline. It ensures the underlying database is open

View file

@ -1,41 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.room.timeline
import androidx.paging.PagedList
/**
* This data class is a holder for timeline data.
* It's returned by [TimelineService]
*/
data class TimelineData(
/**
* The [PagedList] of [TimelineEvent] to usually be render in a RecyclerView.
*/
val events: PagedList<TimelineEvent>,
/**
* True if Timeline is currently paginating forward on server
*/
val isLoadingForward: Boolean = false,
/**
* True if Timeline is currently paginating backward on server
*/
val isLoadingBackward: Boolean = false
)

View file

@ -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 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. * Users can also enrich it with metadata.
*/ */
data class TimelineEvent( data class TimelineEvent(

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -50,10 +50,6 @@ class HomeModule {
HomeNavigator() HomeNavigator()
} }
scope(HOME_SCOPE) {
HomePermalinkHandler(get(), get())
}
// Fragment scopes // Fragment scopes
factory { factory {
@ -98,6 +94,10 @@ class HomeModule {
GroupSummaryController() GroupSummaryController()
} }
scope(ROOM_DETAIL_SCOPE) {
PermalinkHandler(get(), get())
}
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) -> scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
val commandController = AutocompleteCommandController(get()) val commandController = AutocompleteCommandController(get())
AutocompleteCommandPresenter(fragment.requireContext(), commandController) AutocompleteCommandPresenter(fragment.requireContext(), commandController)

View file

@ -17,11 +17,9 @@
package im.vector.riotredesign.features.home package im.vector.riotredesign.features.home
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.fragment.app.FragmentManager
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceFragment import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.features.navigation.Navigator
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import timber.log.Timber import timber.log.Timber
@ -29,48 +27,14 @@ class HomeNavigator {
var activity: HomeActivity? = null var activity: HomeActivity? = null
private var rootRoomId: String? = null
fun openSelectedGroup(groupSummary: GroupSummary) { fun openSelectedGroup(groupSummary: GroupSummary) {
Timber.v("Open selected group ${groupSummary.groupId}") Timber.v("Open selected group ${groupSummary.groupId}")
activity?.let { activity?.let {
it.drawerLayout?.closeDrawer(GravityCompat.START)
val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl) val args = HomeDetailParams(groupSummary.groupId, groupSummary.displayName, groupSummary.avatarUrl)
val homeDetailFragment = HomeDetailFragment.newInstance(args) val homeDetailFragment = HomeDetailFragment.newInstance(args)
it.drawerLayout?.closeDrawer(GravityCompat.START)
it.replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) 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.
it.drawerLayout?.closeDrawer(GravityCompat.START)
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
}
}

View file

@ -1,56 +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.riotredesign.features.home
import android.net.Uri
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.riotredesign.features.navigation.Navigator
class HomePermalinkHandler(private val homeNavigator: HomeNavigator,
private val navigator: Navigator) {
fun launch(deepLink: String?) {
val uri = deepLink?.let { Uri.parse(it) }
launch(uri)
}
fun launch(deepLink: Uri?) {
if (deepLink == null) {
return
}
val permalinkData = PermalinkParser.parse(deepLink)
when (permalinkData) {
is PermalinkData.EventLink -> {
homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, navigator)
}
is PermalinkData.RoomLink -> {
homeNavigator.openRoomDetail(permalinkData.roomIdOrAlias, null, navigator)
}
is PermalinkData.GroupLink -> {
homeNavigator.openGroupDetail(permalinkData.groupId)
}
is PermalinkData.UserLink -> {
homeNavigator.openUserDetail(permalinkData.userId)
}
is PermalinkData.FallbackLink -> {
}
}
}
}

View file

@ -0,0 +1,87 @@
/*
* 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
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.matrix.android.api.session.Session
import im.vector.riotredesign.features.navigation.Navigator
class PermalinkHandler(private val session: Session,
private val navigator: Navigator) {
fun launch(context: Context, deepLink: String?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
val uri = deepLink?.let { Uri.parse(it) }
return launch(context, uri, navigateToRoomInterceptor)
}
fun launch(context: Context, deepLink: Uri?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
if (deepLink == null) {
return false
}
return when (val permalinkData = PermalinkParser.parse(deepLink)) {
is PermalinkData.EventLink -> {
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias, permalinkData.eventId) != true) {
openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId)
}
true
}
is PermalinkData.RoomLink -> {
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 -> {
false
}
}
}
/**
* Open room either joined, or not unknown
*/
private fun openRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) {
if (session.getRoom(roomIdOrAlias) != null) {
navigator.openRoom(context, roomIdOrAlias, eventId)
} else {
navigator.openNotJoinedRoom(context, roomIdOrAlias, eventId)
}
}
}
interface NavigateToRoomInterceptor {
/**
* Return true if the navigation has been intercepted
*/
fun navToRoom(roomId: String, eventId: String? = null): Boolean
}

View file

@ -46,8 +46,8 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
groupController.callback = this groupController.callback = this
stateView.contentView = epoxyRecyclerView stateView.contentView = groupListEpoxyRecyclerView
epoxyRecyclerView.setController(groupController) groupListEpoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) { viewModel.openGroupLiveData.observeEvent(this) {
homeNavigator.openSelectedGroup(it) homeNavigator.openSelectedGroup(it)

View file

@ -84,7 +84,8 @@ class GroupListViewModel(initialState: GroupListViewState,
private fun observeGroupSummaries() { private fun observeGroupSummaries() {
session session
.rx().liveGroupSummaries() .rx()
.liveGroupSummaries()
.map { .map {
val myUser = session.getUser(session.sessionParams.credentials.userId) val myUser = session.getUser(session.sessionParams.credentials.userId)
val allCommunityGroup = GroupSummary( val allCommunityGroup = GroupSummary(
@ -94,6 +95,7 @@ class GroupListViewModel(initialState: GroupListViewState,
listOf(allCommunityGroup) + it listOf(allCommunityGroup) + it
} }
.execute { async -> .execute { async ->
// TODO Phase2 Handle the case where the selected group is deleted on another client
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull() val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()
copy(asyncGroups = async, selectedGroup = newSelectedGroup) copy(asyncGroups = async, selectedGroup = newSelectedGroup)
} }

View file

@ -32,6 +32,7 @@ sealed class RoomDetailActions {
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : 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 UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions()
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : 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 AcceptInvite : RoomDetailActions()
object RejectInvite : RoomDetailActions() object RejectInvite : RoomDetailActions()

View file

@ -43,6 +43,7 @@ import butterknife.BindView
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar 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.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.hideKeyboard import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent 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.glide.GlideApp
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.* 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.command.CommandAutocompletePolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command import im.vector.riotredesign.features.command.Command
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.*
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomePermalinkHandler
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.TextComposerActions
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
@ -167,9 +166,10 @@ class RoomDetailFragment :
private val commandAutocompletePolicy = CommandAutocompletePolicy() private val commandAutocompletePolicy = CommandAutocompletePolicy()
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) } private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
private val autocompleteUserPresenter: AutocompleteUserPresenter 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 private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
override fun getLayoutResId() = R.layout.fragment_room_detail override fun getLayoutResId() = R.layout.fragment_room_detail
@ -199,6 +199,11 @@ class RoomDetailFragment :
handleActions(it) handleActions(it)
} }
roomDetailViewModel.navigateToEvent.observeEvent(this) {
//
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
}
roomDetailViewModel.selectSubscribe( roomDetailViewModel.selectSubscribe(
RoomDetailViewState::sendMode, RoomDetailViewState::sendMode,
RoomDetailViewState::selectedEvent, RoomDetailViewState::selectedEvent,
@ -297,12 +302,14 @@ class RoomDetailFragment :
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { timelineEventController.addModelBuildListener {
it.dispatchTo(stateRestorer) it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback)
} }
recyclerView.addOnScrollListener( recyclerView.addOnScrollListener(
@ -467,7 +474,7 @@ class RoomDetailFragment :
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
timelineEventController.setTimeline(state.timeline) timelineEventController.setTimeline(state.timeline, state.eventId)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE
val uid = session.sessionParams.credentials.userId val uid = session.sessionParams.credentials.userId
@ -486,12 +493,7 @@ class RoomDetailFragment :
state.asyncRoomSummary()?.let { state.asyncRoomSummary()?.let {
roomToolbarTitleView.text = it.displayName roomToolbarTitleView.text = it.displayName
AvatarRenderer.render(it, roomToolbarAvatarImageView) AvatarRenderer.render(it, roomToolbarAvatarImageView)
if (it.topic.isNotEmpty()) { roomToolbarSubtitleView.setTextOrHide(it.topic)
roomToolbarSubtitleView.visibility = View.VISIBLE
roomToolbarSubtitleView.text = it.topic
} else {
roomToolbarSubtitleView.visibility = View.GONE
}
} }
} }
@ -534,8 +536,31 @@ class RoomDetailFragment :
// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String) { override fun onUrlClicked(url: String): Boolean {
homePermalinkHandler.launch(url) 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) { override fun onEventVisible(event: TimelineEvent) {
@ -547,11 +572,13 @@ class RoomDetailFragment :
} }
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent) startActivity(intent)
} }
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent) startActivity(intent)
} }
@ -762,7 +789,7 @@ class RoomDetailFragment :
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) 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) val snack = Snackbar.make(view!!, message, duration)
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show() snack.show()

View file

@ -42,6 +42,7 @@ import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -60,7 +61,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} else { } else {
TimelineDisplayableEvents.DISPLAYABLE_TYPES TimelineDisplayableEvents.DISPLAYABLE_TYPES
} }
private val timeline = room.createTimeline(eventId, allowedTypes) private var timeline = room.createTimeline(eventId, allowedTypes)
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> { companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
@ -98,6 +99,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
is RoomDetailActions.EnterEditMode -> handleEditAction(action) is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action)
else -> Timber.e("Unhandled Action: $action")
} }
} }
@ -128,6 +131,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>> val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
get() = _sendMessageResultLiveData get() = _sendMessageResultLiveData
private val _navigateToEvent = MutableLiveData<LiveEvent<String>>()
val navigateToEvent: LiveData<LiveEvent<String>>
get() = _navigateToEvent
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleSendMessage(action: RoomDetailActions.SendMessage) { 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() { private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second // We are buffering scroll events for one second

View file

@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary 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.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.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
@ -46,7 +45,6 @@ data class RoomDetailViewState(
val timeline: Timeline? = null, val timeline: Timeline? = null,
val asyncInviter: Async<User> = Uninitialized, val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val asyncTimelineData: Async<TimelineData> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR, val sendMode: SendMode = SendMode.REGULAR,
val selectedEvent: TimelineEvent? = null val selectedEvent: TimelineEvent? = null
) : MvRxState { ) : MvRxState {

View file

@ -0,0 +1,50 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import java.util.concurrent.atomic.AtomicReference
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager,
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
private val scheduledEventId = AtomicReference<String?>()
override fun onChanged(position: Int, count: Int, tag: Any?) {
val eventId = scheduledEventId.get() ?: return
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
if (positionToScroll != null) {
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
// Do not scroll it item is already visible
if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
// Note: Offset will be from the bottom, since the layoutManager is reversed
layoutManager.scrollToPositionWithOffset(positionToScroll, 120)
}
scheduledEventId.set(null)
}
}
fun scheduleScrollTo(eventId: String?) {
scheduledEventId.set(eventId)
}
}

View file

@ -46,9 +46,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback { interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onUrlClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.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) fun onMemberNameClicked(informationData: MessageInformationData)
} }
interface UrlClickCallback {
fun onUrlClicked(url: String): Boolean
fun onUrlLongClicked(url: String): Boolean
}
private val collapsedEventIds = linkedSetOf<String>() private val collapsedEventIds = linkedSetOf<String>()
private val mergeItemCollapseStates = HashMap<String, Boolean>() private val mergeItemCollapseStates = HashMap<String, Boolean>()
private val modelCache = arrayListOf<CacheItemData?>() private val modelCache = arrayListOf<CacheItemData?>()
@ -84,39 +88,43 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val listUpdateCallback = object : ListUpdateCallback { private val listUpdateCallback = object : ListUpdateCallback {
@Synchronized
override fun onChanged(position: Int, count: Int, payload: Any?) { override fun onChanged(position: Int, count: Int, payload: Any?) {
assertUpdateCallbacksAllowed() synchronized(modelCache) {
(position until (position + count)).forEach { assertUpdateCallbacksAllowed()
modelCache[it] = null (position until (position + count)).forEach {
modelCache[it] = null
}
requestModelBuild()
} }
requestModelBuild()
} }
@Synchronized
override fun onMoved(fromPosition: Int, toPosition: Int) { override fun onMoved(fromPosition: Int, toPosition: Int) {
assertUpdateCallbacksAllowed() synchronized(modelCache) {
val model = modelCache.removeAt(fromPosition) assertUpdateCallbacksAllowed()
modelCache.add(toPosition, model) val model = modelCache.removeAt(fromPosition)
requestModelBuild() modelCache.add(toPosition, model)
requestModelBuild()
}
} }
@Synchronized
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
assertUpdateCallbacksAllowed() synchronized(modelCache) {
(0 until count).forEach { assertUpdateCallbacksAllowed()
modelCache.add(position, null) (0 until count).forEach {
modelCache.add(position, null)
}
requestModelBuild()
} }
requestModelBuild()
} }
@Synchronized
override fun onRemoved(position: Int, count: Int) { override fun onRemoved(position: Int, count: Int) {
assertUpdateCallbacksAllowed() synchronized(modelCache) {
(0 until count).forEach { assertUpdateCallbacksAllowed()
modelCache.removeAt(position) (0 until count).forEach {
modelCache.removeAt(position)
}
requestModelBuild()
} }
requestModelBuild()
} }
} }
@ -124,13 +132,37 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
requestModelBuild() requestModelBuild()
} }
fun setTimeline(timeline: Timeline?) { fun setTimeline(timeline: Timeline?, eventIdToHighlight: String?) {
if (this.timeline != timeline) { if (this.timeline != timeline) {
this.timeline = timeline this.timeline = timeline
this.timeline?.listener = this this.timeline?.listener = this
// Clear cache
synchronized(modelCache) {
for (i in 0 until modelCache.size) {
modelCache[i] = null
}
}
}
if (this.eventIdToHighlight != eventIdToHighlight) {
// Clear cache to force a refresh
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
requestModelBuild()
} }
} }
private var eventIdToHighlight: String? = null
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView) super.onAttachedToRecyclerView(recyclerView)
timelineMediaSizeProvider.recyclerView = recyclerView timelineMediaSizeProvider.recyclerView = recyclerView
@ -170,28 +202,29 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
require(inSubmitList || Looper.myLooper() == backgroundHandler.looper) require(inSubmitList || Looper.myLooper() == backgroundHandler.looper)
} }
@Synchronized
private fun getModels(): List<EpoxyModel<*>> { private fun getModels(): List<EpoxyModel<*>> {
(0 until modelCache.size).forEach { position -> synchronized(modelCache) {
// Should be build if not cached or if cached but contains mergedHeader or formattedDay (0 until modelCache.size).forEach { position ->
// We then are sure we always have items up to date. // Should be build if not cached or if cached but contains mergedHeader or formattedDay
if (modelCache[position] == null // We then are sure we always have items up to date.
|| modelCache[position]?.mergedHeaderModel != null if (modelCache[position] == null
|| modelCache[position]?.formattedDayModel != null) { || modelCache[position]?.mergedHeaderModel != null
modelCache[position] = buildItemModels(position, currentSnapshot) || 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)
} }
.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()
}
} }
@ -202,14 +235,14 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
val nextDate = nextEvent?.root?.localDateTime() val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() 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.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
} }
val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition) val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition)
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) 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? { private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? {
@ -221,6 +254,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, private fun buildMergedHeaderItem(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
items: List<TimelineEvent>, items: List<TimelineEvent>,
@ -270,10 +304,23 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
addIf(shouldAdd, this@TimelineEventController) addIf(shouldAdd, this@TimelineEventController)
} }
fun searchPositionOfEvent(eventId: String): Int? {
synchronized(modelCache) {
// Search in the cache
modelCache.forEachIndexed { idx, cacheItemData ->
if (cacheItemData?.eventId == eventId) {
return idx
}
}
return null
}
}
} }
private data class CacheItemData( private data class CacheItemData(
val localId: String, val localId: String,
val eventId: String?,
val eventModel: EpoxyModel<*>? = null, val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: MergedHeaderItem? = null, val mergedHeaderModel: MergedHeaderItem? = null,
val formattedDayModel: DaySeparatorItem? = null val formattedDayModel: DaySeparatorItem? = null

View file

@ -22,13 +22,15 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultIte
class DefaultItemFactory { 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) { val text = if (exception == null) {
"${event.root.getClearType()} events are not yet handled" "${event.root.getClearType()} events are not yet handled"
} else { } else {
"an exception occurred when rendering the event ${event.root.eventId}" "an exception occurred when rendering the event ${event.root.eventId}"
} }
return DefaultItem_().text(text) return DefaultItem_()
.text(text)
.highlighted(highlight)
} }
} }

View file

@ -37,6 +37,7 @@ class EncryptedItemFactory(private val messageInformationDataFactory: MessageInf
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
event.root.eventId ?: return null event.root.eventId ?: return null
@ -62,7 +63,9 @@ class EncryptedItemFactory(private val messageInformationDataFactory: MessageInf
return MessageTextItem_() return MessageTextItem_()
.message(spannableStr) .message(spannableStr)
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.urlClickCallback(callback)
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onEncryptedMessageClicked(informationData, view) callback?.onEncryptedMessageClicked(informationData, view)

View file

@ -33,6 +33,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
class EncryptionItemFactory(private val stringProvider: StringProvider) { class EncryptionItemFactory(private val stringProvider: StringProvider) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.BaseCallback?): NoticeItem? { callback: TimelineEventController.BaseCallback?): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null val text = buildNoticeText(event.root, event.senderName) ?: return null
val informationData = MessageInformationData( val informationData = MessageInformationData(
@ -46,6 +47,7 @@ class EncryptionItemFactory(private val stringProvider: StringProvider) {
return NoticeItem_() return NoticeItem_()
.noticeText(text) .noticeText(text)
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.baseCallback(callback) .baseCallback(callback)
} }

View file

@ -56,6 +56,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
event.root.eventId ?: return null event.root.eventId ?: return null
@ -64,7 +65,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
if (event.root.unsignedData?.redactedEvent != null) { if (event.root.unsignedData?.redactedEvent != null) {
//message is redacted //message is redacted
return buildRedactedItem(informationData, callback) return buildRedactedItem(informationData, highlight, callback)
} }
val messageContent: MessageContent = val messageContent: MessageContent =
@ -83,27 +84,31 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData, informationData,
event.annotations?.editSummary, event.annotations?.editSummary,
highlight,
callback) callback)
is MessageTextContent -> buildTextMessageItem(event.sendState, is MessageTextContent -> buildTextMessageItem(event.sendState,
messageContent, messageContent,
informationData, informationData,
event.annotations?.editSummary, event.annotations?.editSummary,
highlight,
callback callback
) )
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback)
else -> buildNotHandledMessageItem(messageContent) else -> buildNotHandledMessageItem(messageContent, highlight)
} }
} }
private fun buildAudioMessageItem(messageContent: MessageAudioContent, private fun buildAudioMessageItem(messageContent: MessageAudioContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageFileItem? { callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_() return MessageFileItem_()
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.filename(messageContent.body) .filename(messageContent.body)
.iconRes(R.drawable.filetype_audio) .iconRes(R.drawable.filetype_audio)
@ -125,9 +130,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
private fun buildFileMessageItem(messageContent: MessageFileContent, private fun buildFileMessageItem(messageContent: MessageFileContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageFileItem? { callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_() return MessageFileItem_()
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.filename(messageContent.body) .filename(messageContent.body)
.reactionPillCallback(callback) .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" 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, private fun buildImageMessageItem(messageContent: MessageImageContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageImageVideoItem? { callback: TimelineEventController.Callback?): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
@ -170,6 +180,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageImageVideoItem_() return MessageImageVideoItem_()
.playable(messageContent.info?.mimeType == "image/gif") .playable(messageContent.info?.mimeType == "image/gif")
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.mediaData(data) .mediaData(data)
.reactionPillCallback(callback) .reactionPillCallback(callback)
@ -190,6 +201,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
private fun buildVideoMessageItem(messageContent: MessageVideoContent, private fun buildVideoMessageItem(messageContent: MessageVideoContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageImageVideoItem? { callback: TimelineEventController.Callback?): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
@ -211,6 +223,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageImageVideoItem_() return MessageImageVideoItem_()
.playable(true) .playable(true)
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.mediaData(thumbnailData) .mediaData(thumbnailData)
.reactionPillCallback(callback) .reactionPillCallback(callback)
@ -230,6 +243,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
messageContent: MessageTextContent, messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
editSummary: EditAggregatedSummary?, editSummary: EditAggregatedSummary?,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {
val bodyToUse = messageContent.formattedBody?.let { val bodyToUse = messageContent.formattedBody?.let {
@ -248,7 +262,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
} }
} }
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.urlClickCallback(callback)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface) .emojiTypeFace(emojiCompatFontProvider.typeface)
//click on the text //click on the text
@ -298,6 +314,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {
val message = messageContent.body.let { val message = messageContent.body.let {
@ -311,8 +328,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageTextItem_() return MessageTextItem_()
.message(message) .message(message)
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.urlClickCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface) .emojiTypeFace(emojiCompatFontProvider.typeface)
.memberClickListener( .memberClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
@ -331,6 +350,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
informationData: MessageInformationData, informationData: MessageInformationData,
editSummary: EditAggregatedSummary?, editSummary: EditAggregatedSummary?,
highlight: Boolean,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {
val message = messageContent.body.let { val message = messageContent.body.let {
@ -347,8 +367,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
} }
} }
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.urlClickCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface) .emojiTypeFace(emojiCompatFontProvider.typeface)
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
@ -361,9 +383,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
} }
private fun buildRedactedItem(informationData: MessageInformationData, private fun buildRedactedItem(informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): RedactedMessageItem? { callback: TimelineEventController.Callback?): RedactedMessageItem? {
return RedactedMessageItem_() return RedactedMessageItem_()
.informationData(informationData) .informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback) .avatarCallback(callback)
} }

View file

@ -28,6 +28,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) { class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? { callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null val formattedText = eventFormatter.format(event) ?: return null
val informationData = MessageInformationData( val informationData = MessageInformationData(
@ -41,6 +42,7 @@ class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {
return NoticeItem_() return NoticeItem_()
.noticeText(formattedText) .noticeText(formattedText)
.highlighted(highlight)
.informationData(informationData) .informationData(informationData)
.baseCallback(callback) .baseCallback(callback)
} }

View file

@ -37,11 +37,13 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight
val computedModel = try { val computedModel = try {
when (event.root.getClearType()) { when (event.root.getClearType()) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
// State and call // State and call
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
@ -49,16 +51,16 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback) EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
// Crypto // Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event, callback) EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, callback) EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, highlight, callback)
// Unhandled event types (yet) // Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER, EventType.STICKER,
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight)
else -> { else -> {
//These are just for debug to display hidden event, they should be filtered out in normal mode //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_() MessageTextItem_()
.informationData(informationData) .informationData(informationData)
.message("{ \"type\": ${event.root.type} }") .message("{ \"type\": ${event.root.type} }")
.highlighted(highlight)
.longClickListener { view -> .longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false ?: false
@ -89,7 +92,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "failed to create message item") Timber.e(e, "failed to create message item")
defaultItemFactory.create(event, e) defaultItemFactory.create(event, highlight, e)
} }
return (computedModel ?: EmptyItem_()) return (computedModel ?: EmptyItem_())
} }

View file

@ -19,20 +19,28 @@ import android.view.View
import android.view.ViewStub import android.view.ViewStub
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.constraintlayout.widget.Guideline import androidx.constraintlayout.widget.Guideline
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.CheckableView
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() { abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
var avatarStyle: AvatarStyle = Companion.AvatarStyle.SMALL var avatarStyle: AvatarStyle = AvatarStyle.SMALL
// To use for instance when opening a permalink with an eventId
@EpoxyAttribute
var highlighted: Boolean = false
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
//optimize? //optimize?
val px = dpToPx(avatarStyle.avatarSizeDP, holder.view.context) val px = dpToPx(avatarStyle.avatarSizeDP + 8, holder.view.context)
holder.leftGuideline.setGuidelineBegin(px) holder.leftGuideline.setGuidelineBegin(px)
holder.checkableBackground.isChecked = highlighted
} }
@ -46,6 +54,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
abstract class BaseHolder : VectorEpoxyHolder() { abstract class BaseHolder : VectorEpoxyHolder() {
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline) val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
@IdRes @IdRes
abstract fun getStubId(): Int abstract fun getStubId(): Int

View file

@ -24,6 +24,7 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.containsOnlyEmojis 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 im.vector.riotredesign.features.html.PillImageSpan
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -38,15 +39,18 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
var message: CharSequence? = null var message: CharSequence? = null
@EpoxyAttribute @EpoxyAttribute
override lateinit var informationData: MessageInformationData override lateinit var informationData: MessageInformationData
@EpoxyAttribute
var urlClickCallback: TimelineEventController.UrlClickCallback? = null
val mvmtMethod = BetterLinkMovementMethod.newInstance().also { // TODO Move this instantiation somewhere else?
it.setOnLinkClickListener { textView, url -> private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
//Return false to let android manage the click on the link it.setOnLinkClickListener { _, url ->
false //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 //Long clicks are handled by parent, return true to block android to do something with url
true urlClickCallback?.onUrlLongClicked(url) == true
} }
} }

View file

@ -72,7 +72,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
setupRecyclerView() setupRecyclerView()
roomListViewModel.subscribe { renderState(it) } roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) { roomListViewModel.openRoomLiveData.observeEvent(this) {
navigator.openRoom(it, requireActivity()) navigator.openRoom(requireActivity(), it)
} }
createChatFabMenu.listener = this createChatFabMenu.listener = this

View file

@ -19,6 +19,9 @@ package im.vector.riotredesign.features.navigation
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom 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.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotredesign.features.debug.DebugMenuActivity import im.vector.riotredesign.features.debug.DebugMenuActivity
@ -27,16 +30,25 @@ import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.riotredesign.features.settings.VectorSettingsActivity import im.vector.riotredesign.features.settings.VectorSettingsActivity
import timber.log.Timber
class DefaultNavigator : Navigator { class DefaultNavigator : Navigator {
override fun openRoom(roomId: String, context: Context) { override fun openRoom(context: Context, roomId: String, eventId: String?) {
val args = RoomDetailArgs(roomId) val args = RoomDetailArgs(roomId, eventId)
val intent = RoomDetailActivity.newIntent(context, args) val intent = RoomDetailActivity.newIntent(context, args)
context.startActivity(intent) 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) { override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
val intent = RoomPreviewActivity.getIntent(context, publicRoom) val intent = RoomPreviewActivity.getIntent(context, publicRoom)
context.startActivity(intent) context.startActivity(intent)
@ -63,4 +75,12 @@ class DefaultNavigator : Navigator {
override fun openKeysBackupManager(context: Context) { override fun openKeysBackupManager(context: Context) {
context.startActivity(KeysBackupManageActivity.intent(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")
}
} }

View file

@ -21,7 +21,9 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
interface Navigator { interface Navigator {
fun openRoom(roomId: String, context: Context) 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 openRoomPreview(publicRoom: PublicRoom, context: Context)
@ -34,4 +36,8 @@ interface Navigator {
fun openKeysBackupSetup(context: Context, showManualExport: Boolean) fun openKeysBackupSetup(context: Context, showManualExport: Boolean)
fun openKeysBackupManager(context: Context) fun openKeysBackupManager(context: Context)
fun openGroupDetail(groupId: String, context: Context)
fun openUserDetail(userId: String, context: Context)
} }

View file

@ -122,7 +122,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
when (joinState) { when (joinState) {
JoinState.JOINED -> { JoinState.JOINED -> {
navigator.openRoom(publicRoom.roomId, requireActivity()) navigator.openRoom(requireActivity(), publicRoom.roomId)
} }
JoinState.NOT_JOINED, JoinState.NOT_JOINED,
JoinState.JOINING_ERROR -> { JoinState.JOINING_ERROR -> {

View file

@ -100,7 +100,7 @@ class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener {
val async = state.asyncCreateRoomRequest val async = state.asyncCreateRoomRequest
if (async is Success) { if (async is Success) {
// Navigate to freshly created room // Navigate to freshly created room
navigator.openRoom(async(), requireActivity()) navigator.openRoom(requireActivity(), async())
navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Close) navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Close)
} else { } else {

View file

@ -108,7 +108,7 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() {
// Quit this screen // Quit this screen
requireActivity().finish() requireActivity().finish()
// Open room // Open room
navigator.openRoom(roomPreviewData.roomId, requireActivity()) navigator.openRoom(requireActivity(), roomPreviewData.roomId)
} }
} }
} }

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<!-- Draw the BG. -->
<item android:left="6dp" android:right="2dp">
<shape>
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="@color/riotx_header_panel_background_black" />
</shape>
</item>
<item android:gravity="start" android:left="2dp">
<shape>
<size android:width="4dp" />
<corners android:bottomLeftRadius="40dp" android:topLeftRadius="40dp" />
<solid android:color="@color/riotx_accent" />
</shape>
</item>
</layer-list>
</item>
<item android:state_checked="false">
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<!-- Draw the BG. -->
<item android:left="6dp" android:right="2dp">
<shape>
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="@color/riotx_header_panel_background_dark" />
</shape>
</item>
<item android:gravity="start" android:left="2dp">
<shape>
<size android:width="4dp" />
<corners android:bottomLeftRadius="40dp" android:topLeftRadius="40dp" />
<solid android:color="@color/riotx_accent" />
</shape>
</item>
</layer-list>
</item>
<item android:state_checked="false">
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<!-- Draw the BG. -->
<item android:left="6dp" android:right="2dp">
<shape>
<corners android:bottomRightRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="@color/riotx_header_panel_background_light" />
</shape>
</item>
<item android:gravity="start" android:left="2dp">
<shape>
<size android:width="4dp" />
<corners android:bottomLeftRadius="40dp" android:topLeftRadius="40dp" />
<solid android:color="@color/riotx_accent" />
</shape>
</item>
</layer-list>
</item>
<item android:state_checked="false">
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<im.vector.riotredesign.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android" <im.vector.riotredesign.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/stateView" android:id="@+id/stateView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.airbnb.epoxy.EpoxyRecyclerView <com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxyRecyclerView" android:id="@+id/groupListEpoxyRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
tools:listitem="@layout/item_group" />
</im.vector.riotredesign.core.platform.StateView> </im.vector.riotredesign.core.platform.StateView>

View file

@ -5,14 +5,24 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:addStatesFromChildren="true" android:addStatesFromChildren="true"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground">
android:paddingLeft="8dp"
android:paddingRight="8dp"> <im.vector.riotredesign.core.platform.CheckableView
android:id="@+id/messageSelectedBackground"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?riotx_highlighted_message_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView <ImageView
android:id="@+id/messageAvatarImageView" android:id="@+id/messageAvatarImageView"
android:layout_width="44dp" android:layout_width="44dp"
android:layout_height="44dp" android:layout_height="44dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -23,7 +33,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
tools:layout_constraintGuide_begin="44dp" /> tools:layout_constraintGuide_begin="52dp" />
<TextView <TextView
android:id="@+id/messageMemberNameView" android:id="@+id/messageMemberNameView"
@ -54,6 +64,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBaseline_toBaselineOf="@id/messageMemberNameView" app:layout_constraintBaseline_toBaselineOf="@id/messageMemberNameView"

View file

@ -1,19 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:addStatesFromChildren="true" android:addStatesFromChildren="true"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground">
android:paddingLeft="8dp"
android:paddingRight="8dp"> <im.vector.riotredesign.core.platform.CheckableView
android:id="@+id/messageSelectedBackground"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?riotx_highlighted_message_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline <androidx.constraintlayout.widget.Guideline
android:id="@+id/messageStartGuideline" android:id="@+id/messageStartGuideline"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
tools:layout_constraintGuide_begin="44dp" /> tools:layout_constraintGuide_begin="52dp" />
<ViewStub <ViewStub
android:id="@+id/messageContentNoticeStub" android:id="@+id/messageContentNoticeStub"

View file

@ -92,5 +92,7 @@
<!-- Widget banner background --> <!-- Widget banner background -->
<attr name="vctr_widget_banner_background" format="color" /> <attr name="vctr_widget_banner_background" format="color" />
<attr name="riotx_highlighted_message_background" format="reference" />
</declare-styleable> </declare-styleable>
</resources> </resources>

View file

@ -8,5 +8,6 @@
<string name="settings_sdk_version">Matrix SDK Version</string> <string name="settings_sdk_version">Matrix SDK Version</string>
<string name="settings_other_third_party_notices">Other third party notices</string> <string name="settings_other_third_party_notices">Other third party notices</string>
<string name="navigate_to_room_when_already_in_the_room">You are already viewing this room!</string>
</resources> </resources>

View file

@ -266,6 +266,8 @@
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginStart">8dp</item> <item name="android:layout_marginStart">8dp</item>
<item name="android:layout_marginLeft">8dp</item> <item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginEnd">8dp</item>
<item name="android:layout_marginRight">8dp</item>
<item name="android:layout_marginBottom">4dp</item> <item name="android:layout_marginBottom">4dp</item>
<item name="android:layout_marginTop">4dp</item> <item name="android:layout_marginTop">4dp</item>
<item name="layout_constraintBottom_toBottomOf">parent</item> <item name="layout_constraintBottom_toBottomOf">parent</item>

View file

@ -31,6 +31,9 @@
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_black</item> <item name="riotx_fab_label_color">@color/riotx_fab_label_color_black</item>
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item> <item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item>
<!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_black</item>
<!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes --> <!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes -->
<item name="colorPrimary">@color/riotx_accent</item> <item name="colorPrimary">@color/riotx_accent</item>
<!--item name="colorPrimaryVariant">@color/primary_color_dark_light</item--> <!--item name="colorPrimaryVariant">@color/primary_color_dark_light</item-->

View file

@ -30,6 +30,9 @@
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item> <item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item>
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item> <item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item>
<!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_dark</item>
<!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes --> <!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes -->
<item name="colorPrimary">@color/riotx_accent</item> <item name="colorPrimary">@color/riotx_accent</item>
<item name="colorPrimaryVariant">@color/primary_color_dark_light</item> <item name="colorPrimaryVariant">@color/primary_color_dark_light</item>

View file

@ -30,6 +30,9 @@
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_light</item> <item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_light</item>
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item> <item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item>
<!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item>
<!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes --> <!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes -->
<item name="colorPrimary">@color/riotx_accent</item> <item name="colorPrimary">@color/riotx_accent</item>
<!--item name="colorPrimaryVariant">@color/primary_color_dark_light</item--> <!--item name="colorPrimaryVariant">@color/primary_color_dark_light</item-->
@ -153,8 +156,7 @@
<item name="vctr_tabbar_background">@drawable/vector_tabbar_background_light</item> <item name="vctr_tabbar_background">@drawable/vector_tabbar_background_light</item>
<item name="vctr_pill_background_user_id">@drawable/pill_background_user_id_light</item> <item name="vctr_pill_background_user_id">@drawable/pill_background_user_id_light</item>
<item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_light <item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_light</item>
</item>
<item name="vctr_pill_text_color_user_id">@color/riot_primary_text_color_light</item> <item name="vctr_pill_text_color_user_id">@color/riot_primary_text_color_light</item>
<item name="vctr_pill_text_color_room_alias">@android:color/white</item> <item name="vctr_pill_text_color_room_alias">@android:color/white</item>

View file

@ -111,8 +111,7 @@
<item name="vctr_tabbar_background">@drawable/vector_tabbar_background_status</item> <item name="vctr_tabbar_background">@drawable/vector_tabbar_background_status</item>
<item name="vctr_pill_background_user_id">@drawable/pill_background_user_id_status</item> <item name="vctr_pill_background_user_id">@drawable/pill_background_user_id_status</item>
<item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_status <item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_status</item>
</item>
<item name="vctr_pill_text_color_user_id">@color/riot_primary_text_color_status</item> <item name="vctr_pill_text_color_user_id">@color/riot_primary_text_color_status</item>
<item name="vctr_pill_text_color_room_alias">@android:color/white</item> <item name="vctr_pill_text_color_room_alias">@android:color/white</item>