Merge pull request #342 from vector-im/feature/edit_history

Feature/edit history
This commit is contained in:
Valere 2019-07-15 15:15:45 +02:00 committed by GitHub
commit 8901a5e09a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 613 additions and 77 deletions

View File

@ -2,7 +2,7 @@ Changes in RiotX 0.2.1 (2019-XX-XX)
===================================================
Features:
-
- Message Editing: View edit history
Improvements:
- Handle click on redacted events: view source and create permalink

View File

@ -45,6 +45,12 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google()
jcenter()
maven {
url 'https://repo.adobe.com/nexus/content/repositories/public/'
content {
includeGroupByRegex "diff_match_patch"
}
}
}
}

View File

@ -16,6 +16,8 @@
package im.vector.matrix.android.api.session.room.model.relation
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
@ -78,6 +80,11 @@ interface RelationService {
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String = "* $newBodyText"): Cancelable
/**
* Get's the edit history of the given event
*/
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)
/**
* Reply to an event in the timeline (must be in same room)
@ -91,4 +98,6 @@ interface RelationService {
autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
}

View File

@ -18,7 +18,8 @@ package im.vector.matrix.android.internal.network
internal object NetworkConstants {
const val URI_API_PREFIX_PATH = "_matrix/client/"
const val URI_API_PREFIX_PATH_R0 = "_matrix/client/r0/"
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
}
}

View File

@ -22,10 +22,11 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
@ -195,6 +196,20 @@ internal interface RoomAPI {
@Body content: Content?
): Call<SendResponse>
/**
* Paginate relations for event based in normal topological order
*
* @param relationType filter for this relation type
* @param eventType filter for this event type
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
fun getRelations(@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Path("relationType") relationType: String,
@Path("eventType") eventType: String
): Call<RelationsResponse>
/**
* Join the given room.
*

View File

@ -30,8 +30,8 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRo
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
@ -56,7 +56,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
private val setReadMarkersTask: SetReadMarkersTask,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val updateQuickReactionTask: UpdateQuickReactionTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val joinRoomTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask) {
@ -67,7 +67,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
val relationService = DefaultRelationService(context,
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, monarchy, taskExecutor)
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor)
return DefaultRoom(
roomId,

View File

@ -142,4 +142,7 @@ internal abstract class RoomModule {
@Binds
abstract fun bindFileService(fileService: DefaultFileService): FileService
@Binds
abstract fun bindFetchEditHistoryTask(editHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
}

View File

@ -53,6 +53,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C
private val eventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor)
: RelationService {
@ -131,6 +132,13 @@ internal class DefaultRelationService @Inject constructor(private val context: C
}
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, eventId)
fetchEditHistoryTask.configureWith(params)
.dispatchTo(callback)
.executeBy(taskExecutor)
}
override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)?.also {
saveLocalEcho(it)
@ -169,7 +177,8 @@ internal class DefaultRelationService @Inject constructor(private val context: C
EventAnnotationsSummaryEntity.where(realm, eventId)
}
return Transformations.map(liveEntity) { realmResults ->
realmResults.firstOrNull()?.asDomain() ?: EventAnnotationsSummary(eventId, emptyList(), null)
realmResults.firstOrNull()?.asDomain()
?: EventAnnotationsSummary(eventId, emptyList(), null)
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.internal.session.room.relation
import arrow.core.Try
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> {
data class Params(
val roomId: String,
val eventId: String
)
}
internal class DefaultFetchEditHistoryTask @Inject constructor(
private val roomAPI: RoomAPI
) : FetchEditHistoryTask {
override suspend fun execute(params: FetchEditHistoryTask.Params): Try<List<Event>> {
return executeRequest<RelationsResponse> {
apiCall = roomAPI.getRelations(params.roomId, params.eventId, RelationType.REPLACE, EventType.MESSAGE)
}.map { resp ->
resp.chunks
}
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.internal.session.room.relation
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true)
internal data class RelationsResponse(
@Json(name = "chunk") val chunks: List<Event>,
@Json(name = "next_batch") val nextBatch: String?,
@Json(name = "prev_batch") val prevBatch: String?
)

View File

@ -254,6 +254,8 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
implementation 'diff_match_patch:diff_match_patch:current'
// TESTS
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@ -344,6 +344,13 @@ SOFTWARE.
<br/>
Copyright (c) 2018, Jaisel Rahman
</li>
<li>
<b>diff-match-patch</b>
<br/>
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch
</li>
</ul>
<pre>
Apache License

View File

@ -35,10 +35,7 @@ import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragm
import im.vector.riotx.features.home.*
import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.login.LoginActivity
@ -93,6 +90,8 @@ interface ScreenComponent {
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
fun inject(messageMenuFragment: MessageMenuFragment)
fun inject(vectorSettingsActivity: VectorSettingsActivity)

View File

@ -29,11 +29,7 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.HomeActivityViewModel
import im.vector.riotx.features.home.HomeActivityViewModel_AssistedFactory
import im.vector.riotx.features.home.HomeDetailViewModel
import im.vector.riotx.features.home.HomeDetailViewModel_AssistedFactory
import im.vector.riotx.features.home.HomeNavigationViewModel
import im.vector.riotx.features.home.*
import im.vector.riotx.features.home.group.GroupListViewModel
import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
@ -59,7 +55,7 @@ import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module
interface ViewModelModule {
@Binds
fun bindViewModelFactory(factory: VectorViewModelFactory): ViewModelProvider.Factory
@ -156,6 +152,9 @@ interface ViewModelModule {
@Binds
fun bindViewReactionViewModelFactory(factory: ViewReactionViewModel_AssistedFactory): ViewReactionViewModel.Factory
@Binds
fun bindViewEditHistoryViewModelFactory(factory: ViewEditHistoryViewModel_AssistedFactory): ViewEditHistoryViewModel.Factory
@Binds
fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory

View File

@ -51,7 +51,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
var title: String? = null
@EpoxyAttribute
var description: String? = null
var description: CharSequence? = null
@EpoxyAttribute
var style: STYLE = STYLE.NORMAL_TEXT

View File

@ -0,0 +1,42 @@
/*
* 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.riotx.core.ui.list
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
/**
* A generic list item header left aligned with notice color.
*/
@EpoxyModelClass(layout = R.layout.item_generic_header)
abstract class GenericItemHeader : VectorEpoxyModel<GenericItemHeader.Holder>() {
@EpoxyAttribute
var text: String? = null
override fun bind(holder: Holder) {
holder.text.setTextOrHide(text)
}
class Holder : VectorEpoxyHolder() {
val text by bind<TextView>(R.id.itemGenericHeaderText)
}
}

View File

@ -0,0 +1,20 @@
package im.vector.riotx.core.ui.list
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
/**
* A generic list item header left aligned with notice color.
*/
@EpoxyModelClass(layout = R.layout.item_generic_loader)
abstract class GenericLoaderItem : VectorEpoxyModel<GenericLoaderItem.Holder>() {
//Maybe/Later add some style configuration, SMALL/BIG ?
override fun bind(holder: Holder) {}
class Holder : VectorEpoxyHolder()
}

View File

@ -32,7 +32,6 @@ sealed class RoomDetailActions {
data class RedactAction(val targetEventId: 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 add: Boolean) : RoomDetailActions()
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
object AcceptInvite : RoomDetailActions()

View File

@ -85,10 +85,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerView
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
@ -666,10 +663,8 @@ class RoomDetailFragment :
}
override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
editAggregatedSummary?.also {
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
}
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
}
// AutocompleteUserPresenter.Callback

View File

@ -114,7 +114,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
@ -309,22 +308,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
return finalText
}
private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) {
//TODO temporary implementation
val lastReplace = action.editAggregatedSummary.sourceEvents.lastOrNull()?.let {
room.getTimeLineEvent(it)
} ?: return
val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
_nonBlockingPopAlert.postValue(LiveEvent(
Pair(R.string.last_edited_info_message, listOf(
lastReplace.getDisambiguatedDisplayName(),
dateFormat.format(Date(lastReplace.root.originServerTs ?: 0)))
))
)
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))

View File

@ -0,0 +1,93 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
import javax.inject.Inject
/**
* Bottom sheet displaying list of edits for a given event ordered by timestamp
*/
class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
private val viewModel: ViewEditHistoryViewModel by fragmentViewModel(ViewEditHistoryViewModel::class)
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
private val epoxyController by lazy {
ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer)
}
override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
ButterKnife.bind(this, view)
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = context?.getString(R.string.message_edits)
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
}
companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): ViewEditHistoryBottomSheet {
val args = Bundle()
val parcelableArgs = TimelineEventFragmentArgs(
informationData.eventId,
roomId,
informationData
)
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
return ViewEditHistoryBottomSheet().apply { arguments = args }
}
}
}

View File

@ -0,0 +1,155 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import android.content.Context
import android.text.Spannable
import android.text.format.DateUtils
import androidx.core.content.ContextCompat
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.riotx.R
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.ui.list.genericLoaderItem
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotx.features.html.EventHtmlRenderer
import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch
import timber.log.Timber
import java.util.*
/**
* Epoxy controller for reaction event list
*/
class ViewEditHistoryEpoxyController(private val context: Context,
val timelineDateFormatter: TimelineDateFormatter,
val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController<ViewEditHistoryViewState>() {
override fun buildModels(state: ViewEditHistoryViewState) {
when (state.editList) {
is Incomplete -> {
genericLoaderItem {
id("Spinner")
}
}
is Fail -> {
genericFooterItem {
id("failure")
text(context.getString(R.string.unknown_error))
}
}
is Success -> {
state.editList()?.let { renderEvents(it) }
}
}
}
private fun renderEvents(sourceEvents: List<Event>) {
if (sourceEvents.isEmpty()) {
genericItem {
id("footer")
title(context.getString(R.string.no_message_edits_found))
}
} else {
var lastDate: Calendar? = null
sourceEvents.sortedByDescending {
it.originServerTs ?: 0
}.forEachIndexed { index, timelineEvent ->
val evDate = Calendar.getInstance().apply {
timeInMillis = timelineEvent.originServerTs
?: System.currentTimeMillis()
}
if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
//need to display header with day
val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
else timelineDateFormatter.formatMessageDay(timelineEvent.localDateTime())
genericItemHeader {
id(evDate.hashCode())
text(dateString)
}
}
lastDate = evDate
val cContent = getCorrectContent(timelineEvent)
val body = cContent.second?.let { eventHtmlRenderer.render(it) }
?: cContent.first
val nextEvent = if (index + 1 <= sourceEvents.lastIndex) sourceEvents[index + 1] else null
var spannedDiff: Spannable? = null
if (nextEvent != null && cContent.second == null /*No diff for html*/) {
//compares the body
val nContent = getCorrectContent(nextEvent)
val nextBody = nContent.second?.let { eventHtmlRenderer.render(it) }
?: nContent.first
val dmp = diff_match_patch()
val diff = dmp.diff_main(nextBody.toString(), body.toString())
Timber.e("#### Diff: $diff")
dmp.diff_cleanupSemantic(diff)
Timber.e("#### Diff: $diff")
spannedDiff = span {
diff.map {
when (it.operation) {
diff_match_patch.Operation.DELETE -> {
span {
text = it.text
textColor = ContextCompat.getColor(context, R.color.vector_error_color)
textDecorationLine = "line-through"
}
}
diff_match_patch.Operation.INSERT -> {
span {
text = it.text
textColor = ContextCompat.getColor(context, R.color.vector_success_color)
}
}
else -> {
span {
text = it.text
}
}
}
}
}
}
genericItem {
id(timelineEvent.eventId)
title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime()))
description(spannedDiff ?: body)
}
}
}
}
private fun getCorrectContent(event: Event): Pair<String, String?> {
val clearContent = event.getClearContent().toModel<MessageTextContent>()
val newContent = clearContent
?.newContent
?.toModel<MessageTextContent>()
return (newContent?.body ?: clearContent?.body ?: "") to (newContent?.formattedBody
?: clearContent?.formattedBody)
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.riotx.features.home.room.detail.timeline.action
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
data class ViewEditHistoryViewState(
val eventId: String,
val roomId: String,
val editList: Async<List<Event>> = Uninitialized)
: MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId)
}
class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
initialState: ViewEditHistoryViewState,
val session: Session,
val timelineDateFormatter: TimelineDateFormatter
) : VectorViewModel<ViewEditHistoryViewState>(initialState) {
private val roomId = initialState.roomId
private val eventId = initialState.eventId
private val room = session.getRoom(roomId)
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
@AssistedInject.Factory
interface Factory {
fun create(initialState: ViewEditHistoryViewState): ViewEditHistoryViewModel
}
companion object : MvRxViewModelFactory<ViewEditHistoryViewModel, ViewEditHistoryViewState> {
override fun create(viewModelContext: ViewModelContext, state: ViewEditHistoryViewState): ViewEditHistoryViewModel? {
val fragment: ViewEditHistoryBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewEditHistoryViewModelFactory.create(state)
}
}
init {
loadHistory()
}
private fun loadHistory() {
setState { copy(editList = Loading()) }
room.fetchEditHistory(eventId, object : MatrixCallback<List<Event>> {
override fun onFailure(failure: Throwable) {
setState {
copy(editList = Fail(failure))
}
}
override fun onSuccess(data: List<Event>) {
//TODO until supported by API Add original event manually
val withOriginal = data.toMutableList()
room.getTimeLineEvent(eventId)?.let {
withOriginal.add(it.root)
}
setState {
copy(editList = Success(withOriginal))
}
}
})
}
}

View File

@ -21,7 +21,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import butterknife.BindView
import butterknife.ButterKnife
@ -33,7 +32,7 @@ import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.synthetic.main.bottom_sheet_display_reactions.*
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
import javax.inject.Inject
/**
@ -49,14 +48,16 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
private val epoxyController by lazy { ViewReactionsEpoxyController(emojiCompatFontProvider.typeface) }
private val epoxyController by lazy {
ViewReactionsEpoxyController(requireContext(), emojiCompatFontProvider.typeface)
}
override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_display_reactions, container, false)
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
ButterKnife.bind(this, view)
return view
}
@ -67,16 +68,11 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = context?.getString(R.string.reactions)
}
override fun invalidate() = withState(viewModel) {
if (it.mapReactionKeyToMemberList() == null) {
bottomSheetViewReactionSpinner.isVisible = true
bottomSheetViewReactionSpinner.animate()
} else {
bottomSheetViewReactionSpinner.isVisible = false
}
epoxyController.setData(it)
}

View File

@ -16,24 +16,47 @@
package im.vector.riotx.features.home.room.detail.timeline.action
import android.content.Context
import android.graphics.Typeface
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.riotx.R
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericLoaderItem
/**
* Epoxy controller for reaction event list
*/
class ViewReactionsEpoxyController(private val emojiCompatTypeface: Typeface?) : TypedEpoxyController<DisplayReactionsViewState>() {
class ViewReactionsEpoxyController(private val context: Context, private val emojiCompatTypeface: Typeface?)
: TypedEpoxyController<DisplayReactionsViewState>() {
override fun buildModels(state: DisplayReactionsViewState) {
val map = state.mapReactionKeyToMemberList() ?: return
map.forEach {
reactionInfoSimpleItem {
id(it.eventId)
emojiTypeFace(emojiCompatTypeface)
timeStamp(it.timestamp)
reactionKey(it.reactionKey)
authorDisplayName(it.authorName ?: it.authorId)
when (state.mapReactionKeyToMemberList) {
is Incomplete -> {
genericLoaderItem {
id("Spinner")
}
}
is Fail -> {
genericFooterItem {
id("failure")
text(context.getString(R.string.unknown_error))
}
}
is Success -> {
state.mapReactionKeyToMemberList()?.forEach {
reactionInfoSimpleItem {
id(it.eventId)
emojiTypeFace(emojiCompatTypeface)
timeStamp(it.timestamp)
reactionKey(it.reactionKey)
authorDisplayName(it.authorName ?: it.authorId)
}
}
}
}
}
}

View File

@ -7,23 +7,14 @@
android:orientation="vertical">
<TextView
android:id="@+id/bottomSheetTitle"
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:padding="8dp"
android:text="@string/reactions"
android:textColor="?android:textColorSecondary"
android:textSize="16sp" />
<ProgressBar
android:id="@+id/bottomSheetViewReactionSpinner"
style="?android:attr/progressBarStyleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="gone"
tools:visibility="visible" />
android:textSize="16sp"
tools:text="@string/reactions" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/bottom_sheet_display_reactions_list"

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemGenericHeaderText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="4dp"
android:paddingRight="16dp"
android:paddingBottom="4dp"
android:textColor="?vctr_notice_text_color"
android:textSize="15sp"
android:textStyle="bold"
tools:text="Today" />

View File

@ -0,0 +1,6 @@
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/genericProgressSpinner"
style="?android:attr/progressBarStyleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp" />

View File

@ -21,4 +21,8 @@
<string name="riotx_no_registration_notice_colored_part">Use the old app</string>
<string name="message_edits">Message Edits</string>
<string name="no_message_edits_found">No edits found</string>
</resources>