diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 201622b3..0ff0987d 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -18,6 +18,7 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import io.reactivex.Observable @@ -49,6 +50,10 @@ class RxRoom(private val room: Room) { room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) } + fun liveEventReadReceipts(eventId: String): Observable> { + return room.getEventReadReceiptsLive(eventId).asObservable() + } + } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index ab406b8e..d97fc497 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -16,7 +16,9 @@ package im.vector.matrix.android.api.session.room.read +import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.model.ReadReceipt /** * This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level. @@ -39,4 +41,6 @@ interface ReadService { fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) fun isEventRead(eventId: String): Boolean + + fun getEventReadReceiptsLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index dd5d2d3b..cf2627b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService @@ -49,6 +50,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val eventFactory: LocalEchoEventFactory, private val roomSummaryMapper: RoomSummaryMapper, private val timelineEventMapper: TimelineEventMapper, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val taskExecutor: TaskExecutor, private val loadRoomMembersTask: LoadRoomMembersTask, private val inviteTask: InviteTask, @@ -67,7 +69,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) - val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) + val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, readReceiptsSummaryMapper, credentials) val relationService = DefaultRelationService(context, credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 2e30c12e..3df872bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -16,12 +16,21 @@ package im.vector.matrix.android.internal.session.room.read +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.read.ReadService +import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper +import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where @@ -33,6 +42,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, private val setReadMarkersTask: SetReadMarkersTask, + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val credentials: Credentials) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { @@ -67,16 +77,26 @@ internal class DefaultReadService @Inject constructor(private val roomId: String var isEventRead = false monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm + ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + ?: return@doWithRealm val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE isEventRead = eventToCheckIndex <= readReceiptIndex } return isEventRead } + override fun getEventReadReceiptsLive(eventId: String): LiveData> { + val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> + ReadReceiptsSummaryEntity.where(realm, eventId) + } + return Transformations.map(liveEntity) { realmResults -> + realmResults.firstOrNull()?.let { + readReceiptsSummaryMapper.map(it) + } ?: emptyList() + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt similarity index 69% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt rename to vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt index a1104e32..540400d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDateFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/date/VectorDateFormatter.kt @@ -14,15 +14,18 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.helper +package im.vector.riotx.core.date +import android.content.Context +import android.text.format.DateUtils import im.vector.riotx.core.resources.LocaleProvider import org.threeten.bp.LocalDateTime import org.threeten.bp.format.DateTimeFormatter import javax.inject.Inject -class TimelineDateFormatter @Inject constructor (private val localeProvider: LocaleProvider) { +class VectorDateFormatter @Inject constructor(private val context: Context, + private val localeProvider: LocaleProvider) { private val messageHourFormatter by lazy { DateTimeFormatter.ofPattern("H:mm", localeProvider.current()) @@ -39,4 +42,11 @@ class TimelineDateFormatter @Inject constructor (private val localeProvider: Loc return messageDayFormatter.format(localDateTime) } + fun formatRelativeDateTime(time: Long?): String { + if (time == null) { + return "" + } + return DateUtils.getRelativeDateTimeString(context, time, DateUtils.DAY_IN_MILLIS, 2 * DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_WEEKDAY).toString() + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 35cda2e6..d5445862 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -41,7 +41,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsers import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment 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.* +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet +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.ViewEditHistoryBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView @@ -165,6 +170,8 @@ interface ScreenComponent { fun inject(createDirectRoomActivity: CreateDirectRoomActivity) + fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 80410f87..c2c86cad 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -29,7 +29,11 @@ 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.* +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.createdirect.CreateDirectRoomNavigationViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory @@ -39,7 +43,18 @@ import im.vector.riotx.features.home.room.detail.RoomDetailViewModel import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel +import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel_AssistedFactory +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel_AssistedFactory import im.vector.riotx.features.home.room.list.RoomListViewModel import im.vector.riotx.features.home.room.list.RoomListViewModel_AssistedFactory import im.vector.riotx.features.reactions.EmojiChooserViewModel @@ -182,4 +197,8 @@ interface ViewModelModule { @Binds fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory + + @Binds + fun bindDisplayReadReceiptsViewModel(factory: DisplayReadReceiptsViewModel_AssistedFactory): DisplayReadReceiptsViewModel.Factory + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index 12d58614..6293e22b 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -23,6 +23,7 @@ import android.widget.LinearLayout import androidx.core.view.isVisible import butterknife.ButterKnife import im.vector.riotx.R +import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.synthetic.main.view_read_receipts.view.* @@ -48,7 +49,8 @@ class ReadReceiptsView @JvmOverloads constructor( ButterKnife.bind(this) } - fun render(readReceipts: List, avatarRenderer: AvatarRenderer) { + fun render(readReceipts: List, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) { + setOnClickListener(clickListener) if (readReceipts.isNotEmpty()) { isVisible = true for (index in 0 until MAX_RECEIPT_DISPLAYED) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index fad93d38..544f05cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -91,6 +91,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerActions 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.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.action.* import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener @@ -315,17 +316,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -354,9 +355,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -391,26 +392,26 @@ class RoomDetailFragment : if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } @@ -816,6 +817,11 @@ class RoomDetailFragment : }) } + override fun onReadReceiptsClicked(informationData: MessageInformationData) { + DisplayReadReceiptsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") + } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt new file mode 100644 index 00000000..fd9b5f11 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt @@ -0,0 +1,55 @@ +/* + * 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.readreceipts + +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_display_read_receipt) +abstract class DisplayReadReceiptItem : EpoxyModelWithHolder() { + + @EpoxyAttribute var name: String? = null + @EpoxyAttribute var userId: String = "" + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var timestamp: CharSequence? = null + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + + override fun bind(holder: Holder) { + avatarRenderer.render(avatarUrl, userId, name, holder.avatarView) + holder.displayNameView.text = name ?: userId + timestamp?.let { + holder.timestampView.text = it + holder.timestampView.isVisible = true + } ?: run { + holder.timestampView.isVisible = false + } + } + + class Holder : VectorEpoxyHolder() { + val avatarView by bind(R.id.readReceiptAvatar) + val displayNameView by bind(R.id.readReceiptName) + val timestampView by bind(R.id.readReceiptDate) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt new file mode 100644 index 00000000..572954f1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -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.readreceipts + +import android.annotation.SuppressLint +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.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs +import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* +import javax.inject.Inject + +/** + * Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp + */ +class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { + + private val viewModel: DisplayReadReceiptsViewModel by fragmentViewModel() + + @Inject lateinit var displayReadReceiptsViewModelFactory: DisplayReadReceiptsViewModel.Factory + @Inject lateinit var epoxyController: DisplayReadReceiptsController + + @BindView(R.id.bottom_sheet_display_reactions_list) + lateinit var epoxyRecyclerView: EpoxyRecyclerView + + + 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 = getString(R.string.read_receipts_list) + } + + + override fun invalidate() = withState(viewModel) { + epoxyController.setData(it) + } + + companion object { + fun newInstance(roomId: String, informationData: MessageInformationData): DisplayReadReceiptsBottomSheet { + val args = Bundle() + val parcelableArgs = TimelineEventFragmentArgs( + informationData.eventId, + roomId, + informationData + ) + args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + return DisplayReadReceiptsBottomSheet().apply { arguments = args } + + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt new file mode 100644 index 00000000..2c2f9f49 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt @@ -0,0 +1,71 @@ +/* + * 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.readreceipts + +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.Session +import im.vector.riotx.R +import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericFooterItem +import im.vector.riotx.core.ui.list.genericLoaderItem +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +/** + * Epoxy controller for read receipt event list + */ +class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter, + private val stringProvider: StringProvider, + private val session: Session, + private val avatarRender: AvatarRenderer) + : TypedEpoxyController() { + + + override fun buildModels(state: DisplayReadReceiptsViewState) { + when (state.readReceipts) { + is Incomplete -> { + genericLoaderItem { + id("loading") + } + } + is Fail -> { + genericFooterItem { + id("failure") + text(stringProvider.getString(R.string.unknown_error)) + } + } + is Success -> { + state.readReceipts()?.forEach { + val timestamp = dateFormatter.formatRelativeDateTime(it.originServerTs) + DisplayReadReceiptItem_() + .id(it.user.userId) + .userId(it.user.userId) + .avatarUrl(it.user.avatarUrl) + .name(it.user.displayName) + .avatarRenderer(avatarRender) + .timestamp(timestamp) + .addIf(session.myUserId != it.user.userId, this) + } + } + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt new file mode 100644 index 00000000..8423ba4a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewModel.kt @@ -0,0 +1,63 @@ +/* + * 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.readreceipts + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.RxRoom +import im.vector.riotx.core.platform.VectorViewModel + +/** + * Used to display the list of read receipts to a given event + */ +class DisplayReadReceiptsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReadReceiptsViewState, + private val session: Session +) : VectorViewModel(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: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel? { + val fragment: DisplayReadReceiptsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.displayReadReceiptsViewModelFactory.create(state) + } + } + + init { + observeEventAnnotationSummaries() + } + + private fun observeEventAnnotationSummaries() { + RxRoom(room) + .liveEventReadReceipts(eventId) + .execute { + copy(readReceipts = it) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt new file mode 100644 index 00000000..68952b99 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsViewState.kt @@ -0,0 +1,33 @@ +/* + * 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.readreceipts + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs + +data class DisplayReadReceiptsViewState( + val eventId: String, + val roomId: String, + val readReceipts: Async> = Uninitialized +) : MvRxState { + + constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId) + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index e8956309..28a3d100 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModel import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.UserPreferencesProvider @@ -42,7 +43,7 @@ import im.vector.riotx.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime import javax.inject.Inject -class TimelineEventController @Inject constructor(private val dateFormatter: TimelineDateFormatter, +class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val avatarRenderer: AvatarRenderer, @@ -51,7 +52,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim userPreferencesProvider: UserPreferencesProvider ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { - interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback { + interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) @@ -77,6 +78,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim fun onMemberNameClicked(informationData: MessageInformationData) } + interface ReadReceiptsCallback { + fun onReadReceiptsClicked(informationData: MessageInformationData) + } + interface UrlClickCallback { fun onUrlClicked(url: String): Boolean fun onUrlLongClicked(url: String): Boolean @@ -158,7 +163,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -219,8 +224,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -293,7 +298,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // We try to find if one of the item id were used as mergeItemCollapseStates key // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() - val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) ?: true + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt index aefbde43..be3dfc80 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt @@ -49,7 +49,7 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { lateinit var epoxyRecyclerView: EpoxyRecyclerView private val epoxyController by lazy { - ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer) + ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) } override fun injectWith(screenComponent: ScreenComponent) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt index fc11f255..dab43108 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt @@ -33,7 +33,7 @@ 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.core.date.VectorDateFormatter import im.vector.riotx.features.html.EventHtmlRenderer import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch @@ -44,7 +44,7 @@ import java.util.* * Epoxy controller for reaction event list */ class ViewEditHistoryEpoxyController(private val context: Context, - val timelineDateFormatter: TimelineDateFormatter, + val dateFormatter: VectorDateFormatter, val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController() { override fun buildModels(state: ViewEditHistoryViewState) { @@ -84,7 +84,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, 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()) + else dateFormatter.formatMessageDay(timelineEvent.localDateTime()) genericItemHeader { id(evDate.hashCode()) text(dateString) @@ -136,7 +136,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, } genericItem { id(timelineEvent.eventId) - title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime())) + title(dateFormatter.formatMessageHour(timelineEvent.localDateTime())) description(spannedDiff ?: body) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt index 6ad17210..e8b27e18 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt @@ -27,7 +27,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import timber.log.Timber import java.util.* @@ -46,7 +46,7 @@ data class ViewEditHistoryViewState( class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted initialState: ViewEditHistoryViewState, val session: Session, - val timelineDateFormatter: TimelineDateFormatter + val dateFormatter: VectorDateFormatter ) : VectorViewModel(initialState) { private val roomId = initialState.roomId diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt index 6f5a7847..9479c307 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt @@ -16,16 +16,20 @@ package im.vector.riotx.features.home.room.detail.timeline.action -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary import im.vector.matrix.rx.RxRoom -import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.isSingleEmoji -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import io.reactivex.Observable import io.reactivex.Single @@ -54,13 +58,13 @@ data class ReactionInfo( class ViewReactionViewModel @AssistedInject constructor(@Assisted initialState: DisplayReactionsViewState, private val session: Session, - private val timelineDateFormatter: TimelineDateFormatter + private val dateFormatter: VectorDateFormatter ) : VectorViewModel(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") + ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") @AssistedInject.Factory interface Factory { @@ -100,14 +104,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted .fromIterable(summary.sourceEvents) .map { val event = room.getTimeLineEvent(it) - ?: throw RuntimeException("Your eventId is not valid") - val localDate = event.root.localDateTime() + ?: throw RuntimeException("Your eventId is not valid") ReactionInfo( event.root.eventId!!, summary.key, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), - timelineDateFormatter.formatMessageHour(localDate) + dateFormatter.formatRelativeDateTime(event.root.originServerTs) + ) } }.toList() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt index 74b3f492..e3df0b73 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action import android.content.Context import android.graphics.Typeface +import android.text.format.DateUtils import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 5ed7bcb3..71e7627d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -161,6 +161,7 @@ class MessageItemFactory @Inject constructor( .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) + .readReceiptsCallback(callback) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) .reactionPillCallback(callback) @@ -191,6 +192,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .filename(messageContent.body) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .iconRes(R.drawable.filetype_attachment) .cellClickListener( @@ -205,10 +207,6 @@ class MessageItemFactory @Inject constructor( DebouncedClickListener(View.OnClickListener { _ -> callback?.onFileMessageClicked(informationData.eventId, messageContent) })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? { @@ -246,6 +244,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .mediaData(data) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .clickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -297,6 +296,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .mediaData(thumbnailData) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -336,6 +336,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .urlClickCallback(callback) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) //click on the text .cellClickListener( @@ -402,6 +403,7 @@ class MessageItemFactory @Inject constructor( .avatarCallback(callback) .reactionPillCallback(callback) .urlClickCallback(callback) + .readReceiptsCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .memberClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -441,6 +443,7 @@ class MessageItemFactory @Inject constructor( .highlighted(highlight) .avatarCallback(callback) .reactionPillCallback(callback) + .readReceiptsCallback(callback) .urlClickCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( @@ -462,6 +465,7 @@ class MessageItemFactory @Inject constructor( .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) + .readReceiptsCallback(callback) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onEventCellClicked(informationData, null, view) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index b1cb5407..f73a2001 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -44,6 +44,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv .highlighted(highlight) .informationData(informationData) .baseCallback(callback) + .readReceiptsCallback(callback) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 51d2ce92..6f3a0a1e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail.timeline.item -import android.annotation.SuppressLint import android.graphics.Typeface import android.os.Build import android.view.View @@ -70,6 +69,9 @@ abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute var avatarCallback: TimelineEventController.AvatarCallback? = null + @EpoxyAttribute + var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + private val _avatarClickListener = DebouncedClickListener(View.OnClickListener { avatarCallback?.onAvatarClicked(informationData) }) @@ -77,6 +79,9 @@ abstract class AbsMessageItem : BaseEventItem() { avatarCallback?.onMemberNameClicked(informationData) }) + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + readReceiptsCallback?.onReadReceiptsClicked(informationData) + }) var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { @@ -124,7 +129,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 679bfbba..d46b2a8d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -17,7 +17,6 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.os.Parcelable -import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.send.SendState import kotlinx.android.parcel.Parcelize diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index b18a665d..c4b5f042 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -23,6 +23,7 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.core.ui.views.ReadReceiptsView +import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -45,6 +46,13 @@ abstract class NoticeItem : BaseEventItem() { return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true } + @EpoxyAttribute + var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + readReceiptsCallback?.onReadReceiptsClicked(informationData) + }) + override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = noticeText @@ -56,7 +64,7 @@ abstract class NoticeItem : BaseEventItem() { holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer) + holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) } override fun getViewType() = STUB_ID diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt index 2f6a4320..a34c9874 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt @@ -24,7 +24,7 @@ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.isSingleEmoji import im.vector.riotx.features.home.getColorFromUserId -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData @@ -35,7 +35,7 @@ import javax.inject.Inject * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline */ class MessageInformationDataFactory @Inject constructor(private val session: Session, - private val timelineDateFormatter: TimelineDateFormatter, + private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { @@ -55,7 +55,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses || (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo - val time = timelineDateFormatter.formatMessageHour(date) + val time = dateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { @@ -79,12 +79,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses hasBeenEdited = event.hasBeenEdited(), hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, readReceipts = event.readReceipts + .asSequence() .filter { it.user.userId != session.myUserId } .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } + .toList() ) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index 6af62341..38f15974 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -29,13 +29,13 @@ import im.vector.riotx.core.resources.DateProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import me.gujun.android.span.span import javax.inject.Inject class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter, - private val timelineDateFormatter: TimelineDateFormatter, + private val dateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, private val avatarRenderer: AvatarRenderer) { @@ -94,7 +94,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte val currentDate = DateProvider.currentLocalDateTime() val isSameDay = date.toLocalDate() == currentDate.toLocalDate() latestFormattedEvent = if (latestEvent.root.isEncrypted() - && latestEvent.root.mxDecryptionResult == null) { + && latestEvent.root.mxDecryptionResult == null) { stringProvider.getString(R.string.encrypted_message) } else if (latestEvent.root.getClearType() == EventType.MESSAGE) { val senderName = latestEvent.senderName() ?: latestEvent.root.senderId @@ -117,10 +117,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte } } latestEventTime = if (isSameDay) { - timelineDateFormatter.formatMessageHour(date) + dateFormatter.formatMessageHour(date) } else { - //TODO: change this - timelineDateFormatter.formatMessageDay(date) + dateFormatter.formatMessageDay(date) } } return RoomSummaryItem_() diff --git a/vector/src/main/res/layout/item_display_read_receipt.xml b/vector/src/main/res/layout/item_display_read_receipt.xml new file mode 100644 index 00000000..cf3c9a26 --- /dev/null +++ b/vector/src/main/res/layout/item_display_read_receipt.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file