From eb1fa0919f410ea549370d3bfd8d3eb07534f71b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Nov 2020 18:08:46 +0100 Subject: [PATCH 01/36] Room member: add methods to get directly from session --- .../org/matrix/android/sdk/rx/RxSession.kt | 8 +++++ .../sdk/api/session/room/RoomService.kt | 17 +++++++++++ .../session/room/DefaultRoomService.kt | 30 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 03df708c0c..0e5b88adb2 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.pushers.Pusher import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.sync.SyncState @@ -92,6 +93,13 @@ class RxSession(private val session: Session) { } } + fun liveRoomMember(userId: String, roomId: String): Observable> { + return session.getRoomMemberLive(userId, roomId).asObservable() + .startWithCallable { + session.getRoomMember(userId, roomId).toOptional() + } + } + fun liveUsers(): Observable> { return session.getUsersLive().asObservable() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index b772225f51..f30037e5c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.Cancelable @@ -141,4 +142,20 @@ interface RoomService { * - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room */ fun getExistingDirectRoomWithUser(otherUserId: String): String? + + /** + * Get a room member for the tuple {userId,roomId} + * @param userId the userId to look for. + * @param roomId the roomId to look for. + * @return the room member or null + */ + fun getRoomMember(userId: String, roomId: String): RoomMemberSummary? + + /** + * Observe a live room member for the tuple {userId,roomId} + * @param userId the userId to look for. + * @param roomId the roomId to look for. + * @return a LiveData of the optional found room member + */ + fun getRoomMemberLive(userId: String, roomId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index d49c2f120c..28656463c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -17,27 +17,37 @@ package org.matrix.android.sdk.internal.session.room import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.fetchCopied import javax.inject.Inject internal class DefaultRoomService @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, private val createRoomTask: CreateRoomTask, private val joinRoomTask: JoinRoomTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask, @@ -118,4 +128,24 @@ internal class DefaultRoomService @Inject constructor( override fun getChangeMembershipsLive(): LiveData> { return roomChangeMembershipStateDataSource.getLiveStates() } + + override fun getRoomMember(userId: String, roomId: String): RoomMemberSummary? { + val roomMemberEntity = monarchy.fetchCopied { + RoomMemberHelper(it, roomId).getLastRoomMember(userId) + } + return roomMemberEntity?.asDomain() + } + + override fun getRoomMemberLive(userId: String, roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + RoomMemberHelper(realm, roomId).queryRoomMembersEvent() + .equalTo(RoomMemberSummaryEntityFields.USER_ID, userId) + }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } } From 6dfdc77ebd0ef07f34f62c6cb0f6dd5b9ab6e0ee Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 6 Nov 2020 14:41:43 +0100 Subject: [PATCH 02/36] Use room member instead of user when it makes sense --- .../app/features/call/VectorCallViewModel.kt | 2 +- .../call/WebRtcPeerConnectionManager.kt | 15 +-- .../call/conference/JitsiCallViewModel.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 9 +- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../home/room/detail/RoomDetailViewState.kt | 2 +- .../detail/search/SearchResultController.kt | 2 +- .../action/MessageActionsViewModel.kt | 11 ++- .../timeline/factory/MessageItemFactory.kt | 43 +++++---- .../app/features/html/EventHtmlRenderer.kt | 46 ++++++---- .../app/features/html/MxLinkTagHandler.kt | 89 ------------------ .../app/features/html/PillsPostProcessor.kt | 91 +++++++++++++++++++ .../app/features/invite/VectorInviteView.kt | 3 +- .../notifications/NotifiableEventResolver.kt | 2 +- .../NotificationBroadcastReceiver.kt | 2 +- 15 files changed, 179 insertions(+), 142 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/html/MxLinkTagHandler.kt create mode 100644 vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 445f40e5b1..bd16adf3e7 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -137,7 +137,7 @@ class VectorCallViewModel @AssistedInject constructor( session.callSignalingService().getCallWithId(it)?.let { mxCall -> this.call = mxCall mxCall.otherUserId - val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem() + val item: MatrixItem? = session.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem() mxCall.addListener(callStateListener) diff --git a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt index 86b38c1158..5bc87ed1d1 100644 --- a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.util.toMatrixItem import org.webrtc.AudioSource import org.webrtc.AudioTrack import org.webrtc.Camera1Enumerator @@ -330,8 +331,8 @@ class WebRtcPeerConnectionManager @Inject constructor( currentCall?.mxCall ?.takeIf { it.state is CallState.Connected } ?.let { mxCall -> - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() - ?: mxCall.roomId + val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() + ?: mxCall.otherUserId // Start background service with notification CallService.onPendingCall( context = context, @@ -388,7 +389,7 @@ class WebRtcPeerConnectionManager @Inject constructor( val mxCall = callContext.mxCall // Update service state - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() + val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() ?: mxCall.roomId CallService.onPendingCall( context = context, @@ -576,7 +577,7 @@ class WebRtcPeerConnectionManager @Inject constructor( ?.let { mxCall -> // Start background service with notification - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() + val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() ?: mxCall.otherUserId CallService.onOnGoingCallBackground( context = context, @@ -650,7 +651,7 @@ class WebRtcPeerConnectionManager @Inject constructor( callAudioManager.startForCall(createdCall) currentCall = callContext - val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName() + val name = currentSession?.getRoomMember(createdCall.otherUserId, createdCall.roomId)?.toMatrixItem()?.getBestName() ?: createdCall.otherUserId CallService.onOutgoingCallRinging( context = context.applicationContext, @@ -706,7 +707,7 @@ class WebRtcPeerConnectionManager @Inject constructor( } // Start background service with notification - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() + val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() ?: mxCall.otherUserId CallService.onIncomingCallRinging( context = context, @@ -845,7 +846,7 @@ class WebRtcPeerConnectionManager @Inject constructor( } val mxCall = call.mxCall // Update service state - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() + val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() ?: mxCall.otherUserId CallService.onPendingCall( context = context, diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index cf4d1417e3..783b519706 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -46,7 +46,7 @@ class JitsiCallViewModel @AssistedInject constructor( } init { - val me = session.getUser(session.myUserId)?.toMatrixItem() + val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem() val userInfo = JitsiMeetUserInfo().apply { displayName = me?.getBestName() avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9c6c473a7f..e1881b5e49 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -141,6 +141,7 @@ import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsB import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan +import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.invite.VectorInviteView import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer @@ -221,7 +222,8 @@ class RoomDetailFragment @Inject constructor( private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, - private val roomDetailPendingActionStore: RoomDetailPendingActionStore + private val roomDetailPendingActionStore: RoomDetailPendingActionStore, + private val pillsPostProcessorFactory: PillsPostProcessor.Factory ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -254,6 +256,9 @@ class RoomDetailFragment @Inject constructor( private val glideRequests by lazy { GlideApp.with(this) } + private val pillsPostProcessor by lazy { + pillsPostProcessorFactory.create(roomDetailArgs.roomId) + } private val autoCompleter: AutoCompleter by lazy { autoCompleterFactory.create(roomDetailArgs.roomId) @@ -848,7 +853,7 @@ class RoomDetailFragment @Inject constructor( if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody ?: messageContent.body) - formattedBody = eventHtmlRenderer.render(document) + formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor) } composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 9efad1081f..04677e7c58 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -1300,7 +1300,7 @@ class RoomDetailViewModel @AssistedInject constructor( } if (summary.membership == Membership.INVITE) { summary.inviterId?.let { inviterId -> - session.getUser(inviterId) + session.getRoomMember(inviterId, summary.roomId) }?.also { setState { copy(asyncInviter = Success(it)) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index b31c972d1a..d7d084f6b7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -60,7 +60,7 @@ data class RoomDetailViewState( val roomId: String, val eventId: String?, val myRoomMember: Async = Uninitialized, - val asyncInviter: Async = Uninitialized, + val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val activeRoomWidgets: Async> = Uninitialized, val typingMessage: String? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index b927fb5ff3..f661aa5ba9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -123,7 +123,7 @@ class SearchResultController @Inject constructor( .formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)) .spannable(spannable) .sender(eventAndSender.sender - ?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem()) + ?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem()) .listener { listener?.onItemClicked(eventAndSender.event) } .let { result.add(it) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 0d1e2261cd..835cecf829 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.app.features.html.EventHtmlRenderer +import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.reactions.data.EmojiDataSource @@ -57,18 +58,22 @@ import java.util.ArrayList * Information related to an event and used to display preview in contextual bottom sheet. */ class MessageActionsViewModel @AssistedInject constructor(@Assisted - initialState: MessageActionState, + private val initialState: MessageActionState, private val eventHtmlRenderer: Lazy, private val htmlCompressor: VectorHtmlCompressor, private val session: Session, private val noticeEventFormatter: NoticeEventFormatter, private val stringProvider: StringProvider, + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { private val eventId = initialState.eventId private val informationData = initialState.informationData private val room = session.getRoom(initialState.roomId) + private val pillsPostProcessor by lazy { + pillsPostProcessorFactory.create(initialState.roomId) + } @AssistedInject.Factory interface Factory { @@ -164,7 +169,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted return when (timelineEvent.root.getClearType()) { EventType.MESSAGE, - EventType.STICKER -> { + EventType.STICKER -> { val messageContent: MessageContent? = timelineEvent.getLastMessageContent() if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { val html = messageContent.formattedBody @@ -172,7 +177,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted ?.let { htmlCompressor.compress(it) } ?: messageContent.body - eventHtmlRenderer.get().render(html) + eventHtmlRenderer.get().render(html, pillsPostProcessor) } else if (messageContent is MessageVerificationRequestContent) { stringProvider.getString(R.string.verification_request) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index dd7a87cce6..2b067ccf3f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -60,6 +60,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.EventHtmlRenderer +import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer @@ -106,15 +107,19 @@ class MessageItemFactory @Inject constructor( private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val session: Session) { + private val pillsPostProcessor by lazy { + pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId) + } + fun create(event: TimelineEvent, nextEvent: TimelineEvent?, highlight: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null - val informationData = messageInformationDataFactory.create(event, nextEvent) if (event.root.isRedacted()) { @@ -139,16 +144,16 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) - is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } @@ -159,7 +164,7 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { return when (messageContent.optionType) { - OPTION_TYPE_POLL -> { + OPTION_TYPE_POLL -> { MessagePollItem_() .attributes(attributes) .callback(callback) @@ -217,13 +222,17 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): VerificationRequestItem? { // If this request is not sent by me or sent to me, we should ignore it in timeline val myUserId = session.myUserId + val roomId = roomSummaryHolder.roomSummary?.roomId if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { return null } val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId - val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName - else informationData.memberName + val otherUserName = if (informationData.sentByMe) { + session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName + } else { + informationData.memberName + } return VerificationRequestItem_() .attributes( VerificationRequestItem.Attributes( @@ -362,7 +371,7 @@ class MessageItemFactory @Inject constructor( val codeVisitor = CodeVisitor() codeVisitor.visit(localFormattedBody) when (codeVisitor.codeKind) { - CodeVisitor.Kind.BLOCK -> { + CodeVisitor.Kind.BLOCK -> { val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) if (codeFormattedBlock == null) { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) @@ -378,7 +387,7 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) } } - CodeVisitor.Kind.NONE -> { + CodeVisitor.Kind.NONE -> { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) } } @@ -393,7 +402,7 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { val compressed = htmlCompressor.compress(messageContent.formattedBody!!) - val formattedBody = htmlRenderer.get().render(compressed) + val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes) } @@ -528,7 +537,7 @@ class MessageItemFactory @Inject constructor( private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence { return matrixFormattedBody ?.let { htmlCompressor.compress(it) } - ?.let { htmlRenderer.get().render(it) } + ?.let { htmlRenderer.get().render(it, pillsPostProcessor) } ?: body } diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index fa644f41b6..d3b3be6a3e 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -17,21 +17,23 @@ package im.vector.app.features.html import android.content.Context -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.glide.GlideApp +import android.text.Spannable +import androidx.core.text.toSpannable import im.vector.app.core.resources.ColorProvider -import im.vector.app.features.home.AvatarRenderer import io.noties.markwon.Markwon import io.noties.markwon.html.HtmlPlugin -import io.noties.markwon.html.TagHandlerNoOp import org.commonmark.node.Node import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton -class EventHtmlRenderer @Inject constructor(context: Context, - htmlConfigure: MatrixHtmlPluginConfigure) { +class EventHtmlRenderer @Inject constructor(htmlConfigure: MatrixHtmlPluginConfigure, + context: Context) { + + interface PostProcessor { + fun afterRender(renderedText: Spannable) + } private val markwon = Markwon.builder(context) .usePlugin(HtmlPlugin.create(htmlConfigure)) @@ -41,35 +43,47 @@ class EventHtmlRenderer @Inject constructor(context: Context, return markwon.parse(text) } - fun render(text: String): CharSequence { + /** + * @param text the text you want to render + * @param postProcessors an optional array of post processor to add any span if needed + */ + fun render(text: String, vararg postProcessors: PostProcessor): CharSequence { return try { - markwon.toMarkdown(text) + val parsed = markwon.parse(text) + renderAndProcess(parsed, postProcessors) } catch (failure: Throwable) { Timber.v("Fail to render $text to html") text } } - fun render(node: Node): CharSequence? { + /** + * @param node the node you want to render + * @param postProcessors an optional array of post processor to add any span if needed + */ + fun render(node: Node, vararg postProcessors: PostProcessor): CharSequence? { return try { - markwon.render(node) + renderAndProcess(node, postProcessors) } catch (failure: Throwable) { Timber.v("Fail to render $node to html") return null } } + + private fun renderAndProcess(node: Node, postProcessors: Array): CharSequence { + val renderedText = markwon.render(node).toSpannable() + postProcessors.forEach { + it.afterRender(renderedText) + } + return renderedText + } } -class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context, - private val colorProvider: ColorProvider, - private val avatarRenderer: AvatarRenderer, - private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure { +class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure { override fun configureHtml(plugin: HtmlPlugin) { plugin - .addHandler(TagHandlerNoOp.create("a")) .addHandler(FontTagHandler()) - .addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session)) .addHandler(MxReplyTagHandler()) .addHandler(SpanHandler(colorProvider)) } diff --git a/vector/src/main/java/im/vector/app/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/app/features/html/MxLinkTagHandler.kt deleted file mode 100644 index 368fdd27ff..0000000000 --- a/vector/src/main/java/im/vector/app/features/html/MxLinkTagHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.html - -import android.content.Context -import android.text.style.URLSpan -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.glide.GlideRequests -import im.vector.app.features.home.AvatarRenderer -import io.noties.markwon.MarkwonVisitor -import io.noties.markwon.SpannableBuilder -import io.noties.markwon.html.HtmlTag -import io.noties.markwon.html.MarkwonHtmlRenderer -import io.noties.markwon.html.tag.LinkHandler -import org.matrix.android.sdk.api.session.permalinks.PermalinkData -import org.matrix.android.sdk.api.session.permalinks.PermalinkParser -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.util.MatrixItem - -class MxLinkTagHandler(private val glideRequests: GlideRequests, - private val context: Context, - private val avatarRenderer: AvatarRenderer, - private val sessionHolder: ActiveSessionHolder) : LinkHandler() { - - override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { - val link = tag.attributes()["href"] - if (link != null) { - val permalinkData = PermalinkParser.parse(link) - val matrixItem = when (permalinkData) { - is PermalinkData.UserLink -> { - val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) - MatrixItem.UserItem(permalinkData.userId, user?.displayName, user?.avatarUrl) - } - is PermalinkData.RoomLink -> { - if (permalinkData.eventId == null) { - val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) - if (permalinkData.isRoomAlias) { - MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) - } else { - MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) - } - } else { - // Exclude event link (used in reply events, we do not want to pill the "in reply to") - null - } - } - is PermalinkData.GroupLink -> { - val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId) - MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl) - } - else -> null - } - - if (matrixItem == null) { - super.handle(visitor, renderer, tag) - } else { - val span = PillImageSpan(glideRequests, avatarRenderer, context, matrixItem) - SpannableBuilder.setSpans( - visitor.builder(), - span, - tag.start(), - tag.end() - ) - SpannableBuilder.setSpans( - visitor.builder(), - URLSpan(link), - tag.start(), - tag.end() - ) - } - } else { - super.handle(visitor, renderer, tag) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt new file mode 100644 index 0000000000..c13f5fdfb3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020 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.app.features.html + +import android.content.Context +import android.text.Spannable +import android.text.Spanned +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.glide.GlideApp +import im.vector.app.features.home.AvatarRenderer +import io.noties.markwon.core.spans.LinkSpan +import org.matrix.android.sdk.api.session.permalinks.PermalinkData +import org.matrix.android.sdk.api.session.permalinks.PermalinkParser +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem + +class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomId: String?, + private val context: Context, + private val avatarRenderer: AvatarRenderer, + private val sessionHolder: ActiveSessionHolder) + : EventHtmlRenderer.PostProcessor { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String?): PillsPostProcessor + } + + override fun afterRender(renderedText: Spannable) { + addPillSpans(renderedText, roomId) + } + + private fun addPillSpans(renderedText: Spannable, roomId: String?) { + // We let markdown handle links and then we add PillImageSpan if needed. + val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java) + linkSpans.forEach { linkSpan -> + val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach + val startSpan = renderedText.getSpanStart(linkSpan) + val endSpan = renderedText.getSpanEnd(linkSpan) + renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? { + val permalinkData = PermalinkParser.parse(url) + val matrixItem = when (permalinkData) { + is PermalinkData.UserLink -> { + if (roomId == null) { + sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem() + } else { + sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem() + } + } + is PermalinkData.RoomLink -> { + if (permalinkData.eventId == null) { + val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) + if (permalinkData.isRoomAlias) { + MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } else { + MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) + } + } else { + // Exclude event link (used in reply events, we do not want to pill the "in reply to") + null + } + } + is PermalinkData.GroupLink -> { + val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId) + MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl) + } + else -> null + } ?: return null + return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem) + } +} diff --git a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt index 881c446eb2..f238261057 100644 --- a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt +++ b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt @@ -27,6 +27,7 @@ import im.vector.app.core.platform.ButtonStateView import im.vector.app.features.home.AvatarRenderer import kotlinx.android.synthetic.main.vector_invite_view.view.* import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -73,7 +74,7 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib } } - fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) { + fun render(sender: RoomMemberSummary, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) { if (mode == Mode.LARGE) { updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT } avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 0740295191..9c2dc9b26d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? { val content = event.content?.toModel() ?: return null val roomId = event.roomId ?: return null - val dName = event.senderId?.let { session.getUser(it)?.displayName } + val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName } if (Membership.INVITE == content.membership) { val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId)) ?: stringProvider.getString(R.string.notification_new_invitation) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 9cfed991bb..d79d16a052 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -120,7 +120,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { null, false, System.currentTimeMillis(), - session.getUser(session.myUserId)?.displayName + session.getRoomMember(session.myUserId, room.roomId)?.displayName ?: context?.getString(R.string.notification_sender_me), session.myUserId, message, From a2a2015af6ee190b578a4a6a7092a73a52f0e24f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 10 Nov 2020 17:17:05 +0300 Subject: [PATCH 03/36] Exclude yourself when decorating rooms which are direct or don't have more than 2 users. --- CHANGES.md | 1 + .../sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fa30acabd5..d61c312fca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Improvements 🙌: Bugfix 🐛: - Fix issue when updating the avatar of a room + - Exclude yourself when decorating rooms which are direct or don't have more than 2 users (#2370) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index f28fe7d642..665d770e7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -241,9 +241,9 @@ internal class UpdateTrustWorker(context: Context, private fun computeRoomShield(activeMemberUserIds: List, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel { Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds") // The set of “all users” depends on the type of room: - // For regular / topic rooms, all users including yourself, are considered when decorating a room + // For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room - val listToCheck = if (roomSummaryEntity.isDirect) { + val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) { activeMemberUserIds.filter { it != myUserId } } else { activeMemberUserIds From c9defec75dd921ac7ba717849eaa3eb37aeb2b37 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Nov 2020 15:18:41 +0100 Subject: [PATCH 04/36] Update CHANGES and clean files --- CHANGES.md | 1 + .../vector/app/features/home/room/detail/RoomDetailViewState.kt | 1 - .../main/java/im/vector/app/features/invite/VectorInviteView.kt | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 26418c75f1..21a5416ee3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Open an existing DM instead of creating a new one (#2319) + - Use RoomMember instead of User in the context of a Room. Bugfix 🐛: - Fix issue when updating the avatar of a room (new avatar vanishing) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index d7d084f6b7..38b93f9363 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.sync.SyncState -import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.widgets.model.Widget /** diff --git a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt index f238261057..5e8c5b3cca 100644 --- a/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt +++ b/vector/src/main/java/im/vector/app/features/invite/VectorInviteView.kt @@ -28,7 +28,6 @@ import im.vector.app.features.home.AvatarRenderer import kotlinx.android.synthetic.main.vector_invite_view.view.* import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject From ccf5d759a4c4a442d1045b48aa7cf05a850f2519 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Nov 2020 15:12:49 +0100 Subject: [PATCH 05/36] Add userConsent to the Identity database and migrate the DB --- .../session/identity/IdentityModule.kt | 4 ++ .../session/identity/data/IdentityData.kt | 3 +- .../session/identity/data/IdentityStore.kt | 2 + .../session/identity/db/IdentityDataEntity.kt | 3 +- .../session/identity/db/IdentityMapper.kt | 3 +- .../session/identity/db/RealmIdentityStore.kt | 8 ++++ .../db/RealmIdentityStoreMigration.kt | 43 +++++++++++++++++++ 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt index e140cc19f3..7a39a333a5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore import io.realm.RealmConfiguration import okhttp3.OkHttpClient +import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStoreMigration import java.io.File @Module @@ -59,6 +60,7 @@ internal abstract class IdentityModule { @SessionScope fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, @SessionFilesDirectory directory: File, + migration: RealmIdentityStoreMigration, @UserMd5 userMd5: String): RealmConfiguration { return RealmConfiguration.Builder() .directory(directory) @@ -66,6 +68,8 @@ internal abstract class IdentityModule { .apply { realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) } + .schemaVersion(RealmIdentityStoreMigration.IDENTITY_STORE_SCHEMA_VERSION) + .migration(migration) .allowWritesOnUiThread(true) .modules(IdentityRealmModule()) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt index 0f04f2fe1a..54d35b34fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityData.kt @@ -20,5 +20,6 @@ internal data class IdentityData( val identityServerUrl: String?, val token: String?, val hashLookupPepper: String?, - val hashLookupAlgorithm: List + val hashLookupAlgorithm: List, + val userConsent: Boolean ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt index 3a905833d5..0e05224be5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/data/IdentityStore.kt @@ -27,6 +27,8 @@ internal interface IdentityStore { fun setToken(token: String?) + fun setUserConsent(consent: Boolean) + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt index cc03465cc8..019289a884 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntity.kt @@ -23,7 +23,8 @@ internal open class IdentityDataEntity( var identityServerUrl: String? = null, var token: String? = null, var hashLookupPepper: String? = null, - var hashLookupAlgorithm: RealmList = RealmList() + var hashLookupAlgorithm: RealmList = RealmList(), + var userConsent: Boolean = false ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt index 98207f1b38..bf23c05811 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityMapper.kt @@ -26,7 +26,8 @@ internal object IdentityMapper { identityServerUrl = entity.identityServerUrl, token = entity.token, hashLookupPepper = entity.hashLookupPepper, - hashLookupAlgorithm = entity.hashLookupAlgorithm.toList() + hashLookupAlgorithm = entity.hashLookupAlgorithm.toList(), + userConsent = entity.userConsent ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt index 0352e9b936..2fa3fc0cfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStore.kt @@ -55,6 +55,14 @@ internal class RealmIdentityStore @Inject constructor( } } + override fun setUserConsent(consent: Boolean) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setUserConsent(realm, consent) + } + } + } + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { Realm.getInstance(realmConfiguration).use { it.executeTransaction { realm -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt new file mode 100644 index 0000000000..6081dbab12 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.identity.db + +import io.realm.DynamicRealm +import io.realm.RealmMigration +import timber.log.Timber +import javax.inject.Inject + +internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigration { + + companion object { + const val IDENTITY_STORE_SCHEMA_VERSION = 1L + } + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.v("Migrating Realm Identity from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Add field userConsent (Boolean) and set the value to false") + + realm.schema.get("IdentityDataEntity") + ?.addField(IdentityDataEntityFields.USER_CONSENT, Boolean::class.java) + } +} From d1e2d065389e7aff3132458cb04aa1353121c38d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Nov 2020 16:07:18 +0100 Subject: [PATCH 06/36] Add userConsent UI to the Discovery screen --- .../api/session/identity/IdentityService.kt | 15 ++++++++ .../session/identity/IdentityServiceError.kt | 1 + .../identity/DefaultIdentityService.kt | 17 ++++++++++ .../identity/db/IdentityDataEntityQuery.kt | 7 ++++ .../vector/app/core/error/ErrorFormatter.kt | 1 + .../discovery/DiscoverySettingsAction.kt | 1 + .../discovery/DiscoverySettingsController.kt | 34 +++++++++++++++++++ .../discovery/DiscoverySettingsFragment.kt | 17 ++++++++++ .../discovery/DiscoverySettingsState.kt | 3 +- .../discovery/DiscoverySettingsViewModel.kt | 30 +++++++++++++--- vector/src/main/res/values/strings.xml | 9 +++++ 11 files changed, 130 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt index 537104a084..908bbcff4a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt @@ -92,9 +92,24 @@ interface IdentityService { /** * Search MatrixId of users providing email and phone numbers + * Note the the user consent has to be set to true, or it will throw a UserConsentNotProvided failure + * Application has to explicitly ask for the user consent. + * Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details. */ fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable + /** + * Return the current user consent + */ + fun getUserConsent(): Boolean + + /** + * Set the user consent. Application may have explicitly ask for the user consent to send their private data + * (email and phone numbers) to the identity server. + * Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details. + */ + fun setUserConsent(newValue: Boolean) + /** * Get the status of the current user's threePid * A lookup will be performed, but also pending binding state will be restored diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt index 72bb72cc2c..42fdb97643 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityServiceError.kt @@ -24,6 +24,7 @@ sealed class IdentityServiceError : Failure.FeatureFailure() { object NoIdentityServerConfigured : IdentityServiceError() object TermsNotSignedException : IdentityServiceError() object BulkLookupSha256NotSupported : IdentityServiceError() + object UserConsentNotProvided : IdentityServiceError() object BindingError : IdentityServiceError() object NoCurrentBindingError : IdentityServiceError() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index 20f8b7f868..9e2eb72375 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.ensureProtocol import kotlinx.coroutines.withContext import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber import javax.inject.Inject import javax.net.ssl.HttpsURLConnection @@ -243,7 +244,20 @@ internal class DefaultIdentityService @Inject constructor( )) } + override fun getUserConsent(): Boolean { + return identityStore.getIdentityData()?.userConsent.orFalse() + } + + override fun setUserConsent(newValue: Boolean) { + identityStore.setUserConsent(newValue) + } + override fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable { + if (!getUserConsent()) { + callback.onFailure(IdentityServiceError.UserConsentNotProvided) + return NoOpCancellable + } + if (threePids.isEmpty()) { callback.onSuccess(emptyList()) return NoOpCancellable @@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor( } override fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable { + // Note: we do not require user consent here, because it is used for email and phone numbers that the user has already sent + // to the home server. Identity server is another service though... + if (threePids.isEmpty()) { callback.onSuccess(emptyMap()) return NoOpCancellable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt index 062c28ea55..5152e33743 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/IdentityDataEntityQuery.kt @@ -52,6 +52,13 @@ internal fun IdentityDataEntity.Companion.setToken(realm: Realm, } } +internal fun IdentityDataEntity.Companion.setUserConsent(realm: Realm, + newConsent: Boolean) { + get(realm)?.apply { + userConsent = newConsent + } +} + internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, pepper: String, algorithms: List) { diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 6065c74541..b9bc935890 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -136,6 +136,7 @@ class DefaultErrorFormatter @Inject constructor( IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error + IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided }) } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt index c66ae69e6a..426f1321e7 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsAction.kt @@ -25,6 +25,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction { object DisconnectIdentityServer : DiscoverySettingsAction() data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction() + data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction() data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt index 306d9bffd1..55c11f3a50 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsController.kt @@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor( buildIdentityServerSection(data) val hasIdentityServer = data.identityServer().isNullOrBlank().not() if (hasIdentityServer && !data.termsNotSigned) { + buildConsentSection(data) buildEmailsSection(data.emailList) buildMsisdnSection(data.phoneNumbersList) } @@ -72,6 +73,38 @@ class DiscoverySettingsController @Inject constructor( } } + private fun buildConsentSection(data: DiscoverySettingsState) { + settingsSectionTitleItem { + id("idConsentTitle") + titleResId(R.string.settings_discovery_consent_title) + } + + if (data.userConsent) { + settingsInfoItem { + id("idConsentInfo") + helperTextResId(R.string.settings_discovery_consent_notice_on) + } + settingsButtonItem { + id("idConsentButton") + colorProvider(colorProvider) + buttonTitleId(R.string.settings_discovery_consent_action_revoke) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonClickListener { listener?.onTapUpdateUserConsent(false) } + } + } else { + settingsInfoItem { + id("idConsentInfo") + helperTextResId(R.string.settings_discovery_consent_notice_off) + } + settingsButtonItem { + id("idConsentButton") + colorProvider(colorProvider) + buttonTitleId(R.string.settings_discovery_consent_action_give_consent) + buttonClickListener { listener?.onTapUpdateUserConsent(true) } + } + } + } + private fun buildIdentityServerSection(data: DiscoverySettingsState) { val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) @@ -359,6 +392,7 @@ class DiscoverySettingsController @Inject constructor( fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) fun onTapChangeIdentityServer() fun onTapDisconnectIdentityServer() + fun onTapUpdateUserConsent(newValue: Boolean) fun onTapRetryToRetrieveBindings() } } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index bfbc00b15a..97d824054d 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -170,6 +170,23 @@ class DiscoverySettingsFragment @Inject constructor( } } + override fun onTapUpdateUserConsent(newValue: Boolean) { + if (newValue) { + withState(viewModel) { state -> + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke())) + .setPositiveButton(R.string.yes) { _, _ -> + viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) + } + .setNegativeButton(R.string.no, null) + .show() + } + } else { + viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false)) + } + } + override fun onTapRetryToRetrieveBindings() { viewModel.handle(DiscoverySettingsAction.RetrieveBinding) } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt index 6b28c07e89..21fbcf1ca7 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsState.kt @@ -25,5 +25,6 @@ data class DiscoverySettingsState( val emailList: Async> = Uninitialized, val phoneNumbersList: Async> = Uninitialized, // Can be true if terms are updated - val termsNotSigned: Boolean = false + val termsNotSigned: Boolean = false, + val userConsent: Boolean = false ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index 0bfcdd9984..0f294e080a 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -63,7 +63,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor( val identityServerUrl = identityService.getCurrentIdentityServerUrl() val currentIS = state.identityServer() setState { - copy(identityServer = Success(identityServerUrl)) + copy( + identityServer = Success(identityServerUrl), + userConsent = false + ) } if (currentIS != identityServerUrl) retrieveBinding() } @@ -71,7 +74,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor( init { setState { - copy(identityServer = Success(identityService.getCurrentIdentityServerUrl())) + copy( + identityServer = Success(identityService.getCurrentIdentityServerUrl()), + userConsent = identityService.getUserConsent() + ) } startListenToIdentityManager() observeThreePids() @@ -97,6 +103,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) + is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action) is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) @@ -105,13 +112,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor( }.exhaustive } + private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) { + identityService.setUserConsent(action.newConsent) + setState { copy(userConsent = action.newConsent) } + } + private fun disconnectIdentityServer() { setState { copy(identityServer = Loading()) } viewModelScope.launch { try { awaitCallback { session.identityService().disconnect(it) } - setState { copy(identityServer = Success(null)) } + setState { + copy( + identityServer = Success(null), + userConsent = false + ) + } } catch (failure: Throwable) { setState { copy(identityServer = Fail(failure)) } } @@ -126,7 +143,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor( val data = awaitCallback { session.identityService().setNewIdentityServer(action.url, it) } - setState { copy(identityServer = Success(data)) } + setState { + copy( + identityServer = Success(data), + userConsent = false + ) + } retrieveBinding() } catch (failure: Throwable) { setState { copy(identityServer = Fail(failure)) } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 45d9d40ba6..4f751a68d0 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1793,6 +1793,14 @@ We sent you a confirm email to %s, check your email and click on the confirmation link We sent you a confirm email to %s, please first check your email and click on the confirmation link Pending + Send emails and phone numbers + You have given your consent to send emails and phone numbers to this identity server to discover other users from your contacts. + You have not given your consent to send emails and phone numbers to this identity server to discover other users from your contacts. + Revoke my consent + Give consent + + Send emails and phone numbers + In order to discover existing contacts you know, do you accept to send your contact data (phone numbers and/or emails) to the configured Identity Server (%1$s)?\n\nFor more privacy, the sent data will be hashed before being sent. Enter an identity server URL Could not connect to identity server @@ -2527,6 +2535,7 @@ For your privacy, Element only supports sending hashed user emails and phone number. The association has failed. The is no current association with this identifier. + The user consent has not been provided. Your homeserver (%1$s) proposes to use %2$s for your identity server Use %1$s From 99bea8f7c321adf493660c61d6d5be566c39f47d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Nov 2020 16:40:33 +0100 Subject: [PATCH 07/36] small change in signature --- .../features/contactsbook/ContactsBookController.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt index 9eca2afa60..59c23f4ac7 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookController.kt @@ -52,11 +52,10 @@ class ContactsBookController @Inject constructor( override fun buildModels() { val currentState = state ?: return - val hasSearch = currentState.searchTerm.isNotEmpty() when (val asyncMappedContacts = currentState.mappedContacts) { is Uninitialized -> renderEmptyState(false) is Loading -> renderLoading() - is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts) + is Success -> renderSuccess(currentState) is Fail -> renderFailure(asyncMappedContacts.error) } } @@ -75,13 +74,13 @@ class ContactsBookController @Inject constructor( } } - private fun renderSuccess(mappedContacts: List, - hasSearch: Boolean, - onlyBoundContacts: Boolean) { + private fun renderSuccess(state: ContactsBookViewState) { + val mappedContacts = state.filteredMappedContacts + if (mappedContacts.isEmpty()) { - renderEmptyState(hasSearch) + renderEmptyState(state.searchTerm.isNotEmpty()) } else { - renderContacts(mappedContacts, onlyBoundContacts) + renderContacts(mappedContacts, state.onlyBoundContacts) } } From 6020f423f4920059fb612e7c5e7d8a55afc79b87 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Nov 2020 17:12:42 +0100 Subject: [PATCH 08/36] Ask for explicit user consent to send their contact details to the identity server (#2375) --- CHANGES.md | 1 + .../contactsbook/ContactsBookAction.kt | 1 + .../contactsbook/ContactsBookFragment.kt | 18 +++++++++++ .../contactsbook/ContactsBookViewModel.kt | 30 ++++++++++++++++--- .../contactsbook/ContactsBookViewState.kt | 8 +++-- .../res/layout/fragment_contacts_book.xml | 23 +++++++++++++- vector/src/main/res/values/strings.xml | 1 + 7 files changed, 75 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4a6133c103..c2c287eb20 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Open an existing DM instead of creating a new one (#2319) + - Ask for explicit user consent to send their contact details to the identity server (#2375) Bugfix 🐛: - Fix issue when restoring draft after sharing (#2287) diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt index 8eb5bc733b..e380998fd2 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookAction.kt @@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class ContactsBookAction : VectorViewModelAction { data class FilterWith(val filter: String) : ContactsBookAction() data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction() + object UserConsentGranted : ContactsBookAction() } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index c4cf9eab39..23d21f5240 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -18,6 +18,7 @@ package im.vector.app.features.contactsbook import android.os.Bundle import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState @@ -57,10 +58,26 @@ class ContactsBookFragment @Inject constructor( sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) setupRecyclerView() setupFilterView() + setupConsentView() setupOnlyBoundContactsView() setupCloseView() } + private fun setupConsentView() { + phoneBookSearchForMatrixContacts.setOnClickListener { + withState(contactsBookViewModel) { state -> + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.identity_server_consent_dialog_title) + .setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: "")) + .setPositiveButton(R.string.yes) { _, _ -> + contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) + } + .setNegativeButton(R.string.no, null) + .show() + } + } + } + private fun setupOnlyBoundContactsView() { phoneBookOnlyBoundContacts.checkedChanges() .subscribe { @@ -98,6 +115,7 @@ class ContactsBookFragment @Inject constructor( } override fun invalidate() = withState(contactsBookViewModel) { state -> + phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved contactsBookController.setData(state) } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt index 167660d11e..2c4c5d0596 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt @@ -38,11 +38,10 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.FoundThreePid +import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber -private typealias PhoneBookSearch = String - class ContactsBookViewModel @AssistedInject constructor(@Assisted initialState: ContactsBookViewState, private val contactsDataSource: ContactsDataSource, @@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted private fun loadContacts() { setState { copy( - mappedContacts = Loading() + mappedContacts = Loading(), + identityServerUrl = session.identityService().getCurrentIdentityServerUrl(), + userConsent = session.identityService().getUserConsent() ) } @@ -109,6 +110,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted } private fun performLookup(data: List) { + if (!session.identityService().getUserConsent()) { + return + } viewModelScope.launch { val threePids = data.flatMap { contact -> contact.emails.map { ThreePid.Email(it.email) } + @@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted } session.identityService().lookUp(threePids, object : MatrixCallback> { override fun onFailure(failure: Throwable) { - // Ignore Timber.w(failure, "Unable to perform the lookup") + + // Should not happen, but just to be sure + if (failure is IdentityServiceError.UserConsentNotProvided) { + setState { + copy(userConsent = false) + } + } } override fun onSuccess(data: List) { @@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted when (action) { is ContactsBookAction.FilterWith -> handleFilterWith(action) is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action) + ContactsBookAction.UserConsentGranted -> handleUserConsentGranted() }.exhaustive } + private fun handleUserConsentGranted() { + session.identityService().setUserConsent(true) + + setState { + copy(userConsent = true) + } + + // Perform the lookup + performLookup(allContacts) + } + private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) { setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt index 3e4f4ddcb6..d2ee684c4d 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewState.kt @@ -26,10 +26,14 @@ data class ContactsBookViewState( val mappedContacts: Async> = Loading(), // Use to filter contacts by display name val searchTerm: String = "", - // Tru to display only bound contacts with their bound 2pid + // True to display only bound contacts with their bound 2pid val onlyBoundContacts: Boolean = false, // All contacts, filtered by searchTerm and onlyBoundContacts val filteredMappedContacts: List = emptyList(), // True when the identity service has return some data - val isBoundRetrieved: Boolean = false + val isBoundRetrieved: Boolean = false, + // The current identity server url if any + val identityServerUrl: String? = null, + // User consent to perform lookup (send emails to the identity server) + val userConsent: Boolean = false ) : MvRxState diff --git a/vector/src/main/res/layout/fragment_contacts_book.xml b/vector/src/main/res/layout/fragment_contacts_book.xml index eb90da1bbe..1f8566e05e 100644 --- a/vector/src/main/res/layout/fragment_contacts_book.xml +++ b/vector/src/main/res/layout/fragment_contacts_book.xml @@ -93,6 +93,27 @@ app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer" tools:visibility="visible" /> + + + + + app:layout_constraintTop_toBottomOf="@+id/phoneBookBottomBarrier" /> Retrieving your contacts… Your contact book is empty Contacts book + Search for contacts on Matrix Revoke invite Revoke invite to %1$s? From daac2e2a1c1f508bb342bbbc61861f4e046adf27 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Nov 2020 17:21:48 +0100 Subject: [PATCH 09/36] Better rational --- .../sdk/internal/session/identity/DefaultIdentityService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index 9e2eb72375..c6fb34151c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -269,8 +269,8 @@ internal class DefaultIdentityService @Inject constructor( } override fun getShareStatus(threePids: List, callback: MatrixCallback>): Cancelable { - // Note: we do not require user consent here, because it is used for email and phone numbers that the user has already sent - // to the home server. Identity server is another service though... + // Note: we do not require user consent here, because it is used for emails and phone numbers that the user has already sent + // to the home server, and not emails and phone numbers from the contact book of the user if (threePids.isEmpty()) { callback.onSuccess(emptyMap()) From b8c89325bc0e660e5f414a379cbd3b9264bd3651 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 11:25:18 +0100 Subject: [PATCH 10/36] Improve Javadoc --- .../sdk/api/session/identity/IdentityService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt index 908bbcff4a..aedb813735 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt @@ -93,20 +93,25 @@ interface IdentityService { /** * Search MatrixId of users providing email and phone numbers * Note the the user consent has to be set to true, or it will throw a UserConsentNotProvided failure - * Application has to explicitly ask for the user consent. + * Application has to explicitly ask for the user consent, and the answer can be stored using [setUserConsent] * Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details. */ fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable /** - * Return the current user consent + * Return the current user consent for the current identity server, which has been stored using [setUserConsent]. + * If [setUserConsent] has not been called, the returned value will be false. + * Note that if the identity server is changed, the user consent is reset to false. + * @return the value stored using [setUserConsent] or false if [setUserConsent] has never been called, or if the identity server + * has been changed */ fun getUserConsent(): Boolean /** - * Set the user consent. Application may have explicitly ask for the user consent to send their private data + * Set the user consent to the provided value. Application MUST explicitly ask for the user consent to send their private data * (email and phone numbers) to the identity server. * Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details. + * @param newValue true if the user explicitly give their consent, false if the user wants to revoke their consent. */ fun setUserConsent(newValue: Boolean) From f2e8a9e0c74ef55f704e3f63ae7cb822bbf59e4e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 18:25:04 +0100 Subject: [PATCH 11/36] Un configure the template, to be able to upgrade Android Studio --- tools/templates/configure.sh | 1 + tools/templates/unconfigure.sh | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100755 tools/templates/unconfigure.sh diff --git a/tools/templates/configure.sh b/tools/templates/configure.sh index 0669ab1312..1b00cef927 100755 --- a/tools/templates/configure.sh +++ b/tools/templates/configure.sh @@ -19,6 +19,7 @@ echo "Configure Element Template..." if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi { +mkdir -p "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other" ln -s $(pwd)/ElementFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other" } && { echo "Please restart Android Studio." diff --git a/tools/templates/unconfigure.sh b/tools/templates/unconfigure.sh new file mode 100755 index 0000000000..36415c50e8 --- /dev/null +++ b/tools/templates/unconfigure.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# +# Copyright 2020 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. +# + +# Template prevent from upgrading Android Studio, so this script de configure the template +echo "Un-configure Element Template..." +if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi + +rm "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other/ElementFeature" +rm -r "${ANDROID_STUDIO%/}/plugins/android/lib/templates" From 64c612dea0f386112c7644931c391e4e5153eaf3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 19:14:50 +0100 Subject: [PATCH 12/36] F-Droid version: ensure timeout of sync request can be more than 60 seconds (#2169) --- CHANGES.md | 1 + .../sdk/internal/network/TimeOutInterceptor.kt | 3 +++ .../android/sdk/internal/session/sync/SyncAPI.kt | 13 ++++++++----- .../android/sdk/internal/session/sync/SyncTask.kt | 12 +++++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9387ae161e..416fa22015 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Improvements 🙌: - Handle events of type "m.room.server_acl" (#890) Bugfix 🐛: + - F-Droid version: ensure timeout of sync request can be more than 60 seconds (#2169) - Fix issue when restoring draft after sharing (#2287) - Fix issue when updating the avatar of a room (new avatar vanishing) - Discard change dialog displayed by mistake when avatar has been updated diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt index 6c604f232f..724ec0dc7f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/TimeOutInterceptor.kt @@ -52,5 +52,8 @@ internal class TimeOutInterceptor @Inject constructor() : Interceptor { const val CONNECT_TIMEOUT = "CONNECT_TIMEOUT" const val READ_TIMEOUT = "READ_TIMEOUT" const val WRITE_TIMEOUT = "WRITE_TIMEOUT" + + // 1 minute + const val DEFAULT_LONG_TIMEOUT: Long = 60_000 } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt index 427a8896c9..77289f04b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncAPI.kt @@ -17,18 +17,21 @@ package org.matrix.android.sdk.internal.session.sync import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import retrofit2.Call import retrofit2.http.GET -import retrofit2.http.Headers +import retrofit2.http.Header import retrofit2.http.QueryMap internal interface SyncAPI { - /** - * Set all the timeouts to 1 minute + * Set all the timeouts to 1 minute by default */ - @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sync") - fun sync(@QueryMap params: Map): Call + fun sync(@QueryMap params: Map, + @Header(TimeOutInterceptor.CONNECT_TIMEOUT) connectTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT, + @Header(TimeOutInterceptor.READ_TIMEOUT) readTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT, + @Header(TimeOutInterceptor.WRITE_TIMEOUT) writeTimeOut: Long = TimeOutInterceptor.DEFAULT_LONG_TIMEOUT + ): Call } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index 303bb45419..b4fd6e7386 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.R import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.filter.FilterRepository @@ -78,8 +79,13 @@ internal class DefaultSyncTask @Inject constructor( // Maybe refresh the home server capabilities data we know getHomeServerCapabilitiesTask.execute(Unit) + val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) + val syncResponse = executeRequest(eventBus) { - apiCall = syncAPI.sync(requestParams) + apiCall = syncAPI.sync( + params = requestParams, + readTimeOut = readTimeOut + ) } syncResponseHandler.handleResponse(syncResponse, token) if (isInitialSync) { @@ -87,4 +93,8 @@ internal class DefaultSyncTask @Inject constructor( } Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") } + + companion object { + private const val TIMEOUT_MARGIN: Long = 10_000 + } } From a056cbd19f0166aad8f5b5a46040f94af4941b39 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Nov 2020 10:54:36 +0100 Subject: [PATCH 13/36] Registration: annoying error message scares every new user when they add an email (#2391) --- CHANGES.md | 1 + .../im/vector/app/features/login/AbstractLoginFragment.kt | 5 +++++ .../main/java/im/vector/app/features/login/LoginViewModel.kt | 1 - 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9387ae161e..80d606f57e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Bugfix 🐛: - Fix issue when restoring draft after sharing (#2287) - Fix issue when updating the avatar of a room (new avatar vanishing) - Discard change dialog displayed by mistake when avatar has been updated + - Registration: annoying error message scares every new user when they add an email (#2391) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt index c6658925af..e3c1aa7b12 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractLoginFragment.kt @@ -69,6 +69,11 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { } override fun showFailure(throwable: Throwable) { + // Only the resumed Fragment can eventually show the error, to avoid multiple dialog display + if (!isResumed) { + return + } + when (throwable) { is Failure.Cancelled -> /* Ignore this error, user has cancelled the action */ diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 81d6a78123..1f47916538 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -207,7 +207,6 @@ class LoginViewModel @AssistedInject constructor( private fun handleCheckIfEmailHasBeenValidated(action: LoginAction.CheckIfEmailHasBeenValidated) { // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state currentTask?.cancel() - currentTask = null currentTask = registrationWizard?.checkIfEmailHasBeenValidated(action.delayMillis, registrationCallback) } From 4dff9316c20f259bfd8f08c2ba5beab9e04c3f35 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sun, 8 Nov 2020 12:53:32 +0000 Subject: [PATCH 14/36] Convert TagsService to suspend functions Signed-off-by: Dominic Fischer --- .../sdk/api/session/room/tags/TagsService.kt | 7 ++----- .../session/room/tags/DefaultTagsService.kt | 21 ++++--------------- .../home/room/list/RoomListViewModel.kt | 11 +++------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt index 3278c640de..69fde61f90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/tags/TagsService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.room.tags -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to handle tags of a room. It's implemented at the room level. */ @@ -26,10 +23,10 @@ interface TagsService { /** * Add a tag to a room */ - fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable + suspend fun addTag(tag: String, order: Double?) /** * Remove tag from a room */ - fun deleteTag(tag: String, callback: MatrixCallback): Cancelable + suspend fun deleteTag(tag: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt index 932cb5d67e..d6c02f0a49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt @@ -18,15 +18,10 @@ package org.matrix.android.sdk.internal.session.room.tags import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.tags.TagsService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultTagsService @AssistedInject constructor( @Assisted private val roomId: String, - private val taskExecutor: TaskExecutor, private val addTagToRoomTask: AddTagToRoomTask, private val deleteTagFromRoomTask: DeleteTagFromRoomTask ) : TagsService { @@ -36,21 +31,13 @@ internal class DefaultTagsService @AssistedInject constructor( fun create(roomId: String): TagsService } - override fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable { + override suspend fun addTag(tag: String, order: Double?) { val params = AddTagToRoomTask.Params(roomId, tag, order) - return addTagToRoomTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + addTagToRoomTask.execute(params) } - override fun deleteTag(tag: String, callback: MatrixCallback): Cancelable { + override suspend fun deleteTag(tag: String) { val params = DeleteTagFromRoomTask.Params(roomId, tag) - return deleteTagFromRoomTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + deleteTagFromRoomTask.execute(params) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index b3af3b5e95..84652506cd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.tag.RoomTag -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.lang.Exception @@ -191,17 +190,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, action.tag.otherTag() ?.takeIf { room.roomSummary()?.hasTag(it).orFalse() } ?.let { tagToRemove -> - awaitCallback { room.deleteTag(tagToRemove, it) } + room.deleteTag(tagToRemove) } // Set the tag. We do not handle the order for the moment - awaitCallback { - room.addTag(action.tag, 0.5, it) - } + room.addTag(action.tag, 0.5) } else { - awaitCallback { - room.deleteTag(action.tag, it) - } + room.deleteTag(action.tag) } } catch (failure: Throwable) { _viewEvents.post(RoomListViewEvents.Failure(failure)) From d67029c42cfe9d76917d62f610c61b948506cd15 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sun, 8 Nov 2020 13:12:07 +0000 Subject: [PATCH 15/36] Convert ReportingService to suspend functions Signed-off-by: Dominic Fischer --- .../session/room/reporting/ReportingService.kt | 5 +---- .../room/reporting/DefaultReportingService.kt | 14 ++------------ .../home/room/detail/RoomDetailViewModel.kt | 17 +++++++++-------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt index 0ccdfd1d3c..a444e2346e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.room.reporting -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to report content of an event. */ @@ -28,5 +25,5 @@ interface ReportingService { * Report content * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid */ - fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable + suspend fun reportContent(eventId: String, score: Int, reason: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt index 384c544ee0..cac87a9d30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt @@ -18,14 +18,9 @@ package org.matrix.android.sdk.internal.session.room.reporting import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.reporting.ReportingService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, - private val taskExecutor: TaskExecutor, private val reportContentTask: ReportContentTask ) : ReportingService { @@ -34,13 +29,8 @@ internal class DefaultReportingService @AssistedInject constructor(@Assisted pri fun create(roomId: String): ReportingService } - override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable { + override suspend fun reportContent(eventId: String, score: Int, reason: String) { val params = ReportContentTask.Params(roomId, eventId, score, reason) - - return reportContentTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + reportContentTask.execute(params) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 102a0673d4..1f22406883 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -99,6 +99,7 @@ import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber import java.io.File +import java.lang.Exception import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -1112,15 +1113,15 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleReportContent(action: RoomDetailAction.ReportContent) { - room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + viewModelScope.launch { + val event = try { + room.reportContent(action.eventId, -100, action.reason) + RoomDetailViewEvents.ActionSuccess(action) + } catch (failure: Exception) { + RoomDetailViewEvents.ActionFailure(action, failure) } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) - } - }) + _viewEvents.post(event) + } } private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) { From 574d5055bd616520dd05c3440715742aa6c14560 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sun, 8 Nov 2020 14:19:47 +0000 Subject: [PATCH 16/36] Convert DraftService to suspend functions Signed-off-by: Dominic Fischer --- .../sdk/api/session/room/send/DraftService.kt | 6 +-- .../session/room/draft/DefaultDraftService.kt | 14 +++---- .../home/room/detail/RoomDetailViewModel.kt | 38 ++++++++++--------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt index 116a60e323..a9481d71a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.api.session.room.send import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional interface DraftService { @@ -26,12 +24,12 @@ interface DraftService { /** * Save or update a draft to the room */ - fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable + suspend fun saveDraft(draft: UserDraft) /** * Delete the last draft, basically just after sending the message */ - fun deleteDraft(callback: MatrixCallback): Cancelable + suspend fun deleteDraft() /** * Return the current draft or null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt index 92e16a3501..93fbfb4df0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt @@ -19,18 +19,14 @@ package org.matrix.android.sdk.internal.session.room.draft import androidx.lifecycle.LiveData import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.UserDraft -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, private val draftRepository: DraftRepository, - private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers ) : DraftService { @@ -43,14 +39,14 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, * or even move an existing draft to the top of the list */ - override fun saveDraft(draft: UserDraft, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + override suspend fun saveDraft(draft: UserDraft) { + withContext(coroutineDispatchers.main) { draftRepository.saveDraft(roomId, draft) } } - override fun deleteDraft(callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + override suspend fun deleteDraft() { + withContext(coroutineDispatchers.main) { draftRepository.deleteDraft(roomId) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 1f22406883..ffaeb1b157 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -476,22 +476,24 @@ class RoomDetailViewModel @AssistedInject constructor( * Convert a send mode to a draft and save the draft */ private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState { - when { - it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { - setState { copy(sendMode = it.sendMode.copy(action.draft)) } - room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.REPLY -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.QUOTE -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.EDIT -> { - setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } - room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + viewModelScope.launch { + when { + it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { + setState { copy(sendMode = it.sendMode.copy(action.draft)) } + room.saveDraft(UserDraft.REGULAR(action.draft)) + } + it.sendMode is SendMode.REPLY -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.QUOTE -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + it.sendMode is SendMode.EDIT -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } } } } @@ -778,7 +780,9 @@ class RoomDetailViewModel @AssistedInject constructor( } else { // Otherwise we clear the composer and remove the draft from db setState { copy(sendMode = SendMode.REGULAR("", false)) } - room.deleteDraft(NoOpMatrixCallback()) + viewModelScope.launch { + room.deleteDraft() + } } } From 0a318f618b2e466e0af801e31ad3796bcc48e5e8 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Fri, 13 Nov 2020 18:20:44 +0000 Subject: [PATCH 17/36] Convert RawService to suspend functions Signed-off-by: Dominic Fischer --- .../matrix/android/sdk/api/raw/RawService.kt | 11 ++----- .../sdk/internal/raw/DefaultRawService.kt | 29 ++++--------------- .../raw/wellknown/ElementWellKnownExt.kt | 3 +- 3 files changed, 10 insertions(+), 33 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt index 4e24a17047..19549a338e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.raw -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * Useful methods to fetch raw data from the server. The access token will not be used to fetched the data */ @@ -26,17 +23,15 @@ interface RawService { /** * Get a URL, either from cache or from the remote server, depending on the cache strategy */ - fun getUrl(url: String, - rawCacheStrategy: RawCacheStrategy, - matrixCallback: MatrixCallback): Cancelable + suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String /** * Specific case for the well-known file. Cache validity is 8 hours */ - fun getWellknown(userId: String, matrixCallback: MatrixCallback): Cancelable + suspend fun getWellknown(userId: String): String /** * Clear all the cache data */ - fun clearCache(matrixCallback: MatrixCallback): Cancelable + suspend fun clearCache() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt index be01366efa..5107ba5b50 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt @@ -16,45 +16,28 @@ package org.matrix.android.sdk.internal.raw -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.raw.RawCacheStrategy import org.matrix.android.sdk.api.raw.RawService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import java.util.concurrent.TimeUnit import javax.inject.Inject internal class DefaultRawService @Inject constructor( - private val taskExecutor: TaskExecutor, private val getUrlTask: GetUrlTask, private val cleanRawCacheTask: CleanRawCacheTask ) : RawService { - override fun getUrl(url: String, - rawCacheStrategy: RawCacheStrategy, - matrixCallback: MatrixCallback): Cancelable { - return getUrlTask - .configureWith(GetUrlTask.Params(url, rawCacheStrategy)) { - callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String { + return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy)) } - override fun getWellknown(userId: String, - matrixCallback: MatrixCallback): Cancelable { + override suspend fun getWellknown(userId: String): String { val homeServerDomain = userId.substringAfter(":") return getUrl( "https://$homeServerDomain/.well-known/matrix/client", - RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false), - matrixCallback + RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) ) } - override fun clearCache(matrixCallback: MatrixCallback): Cancelable { - return cleanRawCacheTask - .configureWith(Unit) { - callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun clearCache() { + return cleanRawCacheTask.execute(Unit) } } diff --git a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt index 119be66f94..c1118e40cb 100644 --- a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt +++ b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt @@ -18,10 +18,9 @@ package im.vector.app.features.raw.wellknown import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService -import org.matrix.android.sdk.internal.util.awaitCallback suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? { - return tryOrNull { awaitCallback { getWellknown(userId, it) } } + return tryOrNull { getWellknown(userId) } ?.let { ElementWellKnownMapper.from(it) } } From 75c105a400ef58546d4791e719657def6770595d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Nov 2020 14:56:42 +0100 Subject: [PATCH 18/36] Move "Enable Encryption" from room setting screen to room profile screen (#2394) --- CHANGES.md | 1 + .../features/roomprofile/RoomProfileAction.kt | 1 + .../roomprofile/RoomProfileController.kt | 28 +++++++++++++++ .../roomprofile/RoomProfileFragment.kt | 20 +++++++++++ .../roomprofile/RoomProfileViewModel.kt | 36 +++++++++++++++++++ .../roomprofile/RoomProfileViewState.kt | 8 ++++- .../settings/RoomSettingsAction.kt | 1 - .../settings/RoomSettingsController.kt | 29 --------------- .../settings/RoomSettingsFragment.kt | 11 ------ .../settings/RoomSettingsViewModel.kt | 18 +--------- .../settings/RoomSettingsViewState.kt | 3 +- .../res/layout/fragment_matrix_profile.xml | 3 +- vector/src/main/res/values/strings.xml | 3 +- 13 files changed, 99 insertions(+), 63 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 936e6b0ffe..3ac90b4c71 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Improvements 🙌: - Open an existing DM instead of creating a new one (#2319) - Ask for explicit user consent to send their contact details to the identity server (#2375) - Handle events of type "m.room.server_acl" (#890) + - Move "Enable Encryption" from room setting screen to room profile screen (#2394) Bugfix 🐛: - Fix issue when restoring draft after sharing (#2287) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt index 85bc8773a5..073d30ff8e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt @@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState sealed class RoomProfileAction : VectorViewModelAction { + object EnableEncryption : RoomProfileAction() object LeaveRoom : RoomProfileAction() data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() object ShareRoomProfile : RoomProfileAction() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index 7dc744da31..891d15d04f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -28,6 +28,7 @@ import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject class RoomProfileController @Inject constructor( @@ -43,6 +44,7 @@ class RoomProfileController @Inject constructor( interface Callback { fun onLearnMoreClicked() + fun onEnableEncryptionClicked() fun onMemberListClicked() fun onBannedMemberListClicked() fun onNotificationsClicked() @@ -84,6 +86,7 @@ class RoomProfileController @Inject constructor( centered(false) text(stringProvider.getString(learnMoreSubtitle)) } + buildEncryptionAction(data.actionPermissions, roomSummary) // More buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) @@ -171,4 +174,29 @@ class RoomProfileController @Inject constructor( ) } } + + private fun buildEncryptionAction(actionPermissions: RoomProfileViewState.ActionPermissions, roomSummary: RoomSummary) { + if (!roomSummary.isEncrypted) { + if (actionPermissions.canEnableEncryption) { + buildProfileAction( + id = "enableEncryption", + title = stringProvider.getString(R.string.room_settings_enable_encryption), + dividerColor = dividerColor, + icon = R.drawable.ic_shield_black, + divider = false, + editable = false, + action = { callback?.onEnableEncryptionClicked() } + ) + } else { + buildProfileAction( + id = "enableEncryption", + title = stringProvider.getString(R.string.room_settings_enable_encryption_no_permission), + dividerColor = dividerColor, + icon = R.drawable.ic_shield_black, + divider = false, + editable = false + ) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 5bd121d49b..bab64aebe9 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -49,6 +49,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.media.BigImageViewerActivity import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_matrix_profile.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.view_stub_room_profile_header.* import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.util.MatrixItem @@ -87,6 +88,7 @@ class RoomProfileFragment @Inject constructor( it.layoutResource = R.layout.view_stub_room_profile_header it.inflate() } + setupWaitingView() setupToolbar(matrixProfileToolbar) setupRecyclerView() appBarStateChangeListener = MatrixItemAppBarStateChangeListener( @@ -111,6 +113,11 @@ class RoomProfileFragment @Inject constructor( setupLongClicks() } + private fun setupWaitingView() { + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + } + private fun setupLongClicks() { roomProfileNameView.copyOnLongClick() roomProfileAliasView.copyOnLongClick() @@ -155,6 +162,8 @@ class RoomProfileFragment @Inject constructor( } override fun invalidate() = withState(roomProfileViewModel) { state -> + waiting_view.isVisible = state.isLoading + state.roomSummary()?.also { if (it.membership.isLeft()) { Timber.w("The room has been left") @@ -187,6 +196,17 @@ class RoomProfileFragment @Inject constructor( vectorBaseActivity.notImplemented() } + override fun onEnableEncryptionClicked() { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.room_settings_enable_encryption_dialog_title) + .setMessage(R.string.room_settings_enable_encryption_dialog_content) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ -> + roomProfileViewModel.handle(RoomProfileAction.EnableEncryption) + } + .show() + } + override fun onMemberListClicked() { roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index a78bf472d6..ec772ffcaa 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -28,12 +28,15 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.ShortcutCreator +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.rx.RxRoom import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -65,6 +68,7 @@ class RoomProfileViewModel @AssistedInject constructor( val rxRoom = room.rx() observeRoomSummary(rxRoom) observeBannedRoomMembers(rxRoom) + observePermissions() } private fun observeRoomSummary(rxRoom: RxRoom) { @@ -82,8 +86,22 @@ class RoomProfileViewModel @AssistedInject constructor( } } + private fun observePermissions() { + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val permissions = RoomProfileViewState.ActionPermissions( + canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + ) + setState { copy(actionPermissions = permissions) } + } + .disposeOnClear() + } + override fun handle(action: RoomProfileAction) { when (action) { + is RoomProfileAction.EnableEncryption -> handleEnableEncryption() RoomProfileAction.LeaveRoom -> handleLeaveRoom() is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() @@ -91,6 +109,24 @@ class RoomProfileViewModel @AssistedInject constructor( }.exhaustive } + private fun handleEnableEncryption() { + postLoading(true) + + viewModelScope.launch { + val result = runCatching { room.enableEncryption() } + postLoading(false) + result.onFailure { failure -> + _viewEvents.post(RoomProfileViewEvents.Failure(failure)) + } + } + } + + private fun postLoading(isLoading: Boolean) { + setState { + copy(isLoading = isLoading) + } + } + private fun handleCreateShortcut() { viewModelScope.launch(Dispatchers.IO) { withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index 50723655bc..398982ede1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -26,8 +26,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary data class RoomProfileViewState( val roomId: String, val roomSummary: Async = Uninitialized, - val bannedMembership: Async> = Uninitialized + val bannedMembership: Async> = Uninitialized, + val actionPermissions: ActionPermissions = ActionPermissions(), + val isLoading: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + + data class ActionPermissions( + val canEnableEncryption: Boolean = false + ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt index 80bb8813cf..f0a7b38478 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt @@ -25,7 +25,6 @@ sealed class RoomSettingsAction : VectorViewModelAction { data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction() - object EnableEncryption : RoomSettingsAction() object Save : RoomSettingsAction() object Cancel : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 5231cc6b06..3c73e6ed46 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -29,7 +29,6 @@ import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibi import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -44,7 +43,6 @@ class RoomSettingsController @Inject constructor( // Delete the avatar, or cancel an avatar change fun onAvatarDelete() fun onAvatarChange() - fun onEnableEncryptionClicked() fun onNameChanged(name: String) fun onTopicChanged(topic: String) fun onHistoryVisibilityClicked() @@ -130,33 +128,6 @@ class RoomSettingsController @Inject constructor( editable = data.actionPermissions.canChangeHistoryReadability, action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() } ) - - buildEncryptionAction(data.actionPermissions, roomSummary) - } - - private fun buildEncryptionAction(actionPermissions: RoomSettingsViewState.ActionPermissions, roomSummary: RoomSummary) { - if (!actionPermissions.canEnableEncryption) { - return - } - if (roomSummary.isEncrypted) { - buildProfileAction( - id = "encryption", - title = stringProvider.getString(R.string.room_settings_addresses_e2e_enabled), - dividerColor = dividerColor, - divider = false, - editable = false - ) - } else { - buildProfileAction( - id = "encryption", - title = stringProvider.getString(R.string.room_settings_enable_encryption), - subtitle = stringProvider.getString(R.string.room_settings_enable_encryption_warning), - dividerColor = dividerColor, - divider = false, - editable = true, - action = { callback?.onEnableEncryptionClicked() } - ) - } } private fun formatRoomHistoryVisibilityEvent(event: Event): String? { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 57521f7d80..ab9d3c6896 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -127,17 +127,6 @@ class RoomSettingsFragment @Inject constructor( invalidateOptionsMenu() } - override fun onEnableEncryptionClicked() { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.room_settings_enable_encryption_dialog_title) - .setMessage(R.string.room_settings_enable_encryption_dialog_content) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.room_settings_enable_encryption_dialog_submit) { _, _ -> - viewModel.handle(RoomSettingsAction.EnableEncryption) - } - .show() - } - override fun onNameChanged(name: String) { viewModel.handle(RoomSettingsAction.SetRoomName(name)) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 086ce93bb0..05a75a585b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -17,7 +17,6 @@ package im.vector.app.features.roomprofile.settings import androidx.core.net.toFile -import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -28,7 +27,6 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Completable import io.reactivex.Observable -import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -118,8 +116,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_CANONICAL_ALIAS), canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_HISTORY_VISIBILITY), - canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + EventType.STATE_ROOM_HISTORY_VISIBILITY) ) setState { copy(actionPermissions = permissions) } } @@ -142,7 +139,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: RoomSettingsAction) { when (action) { - is RoomSettingsAction.EnableEncryption -> handleEnableEncryption() is RoomSettingsAction.SetAvatarAction -> handleSetAvatarAction(action) is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } @@ -226,18 +222,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: ) } - private fun handleEnableEncryption() { - postLoading(true) - - viewModelScope.launch { - val result = runCatching { room.enableEncryption() } - postLoading(false) - result.onFailure { failure -> - _viewEvents.post(RoomSettingsViewEvents.Failure(failure)) - } - } - } - private fun postLoading(isLoading: Boolean) { setState { copy(isLoading = isLoading) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt index f913bed382..2cadc8f798 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt @@ -47,8 +47,7 @@ data class RoomSettingsViewState( val canChangeName: Boolean = false, val canChangeTopic: Boolean = false, val canChangeCanonicalAlias: Boolean = false, - val canChangeHistoryReadability: Boolean = false, - val canEnableEncryption: Boolean = false + val canChangeHistoryReadability: Boolean = false ) sealed class AvatarAction { diff --git a/vector/src/main/res/layout/fragment_matrix_profile.xml b/vector/src/main/res/layout/fragment_matrix_profile.xml index c935ab5cee..c10185b2f3 100644 --- a/vector/src/main/res/layout/fragment_matrix_profile.xml +++ b/vector/src/main/res/layout/fragment_matrix_profile.xml @@ -95,7 +95,6 @@ - + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 48729df815..7eec4eca19 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2200,7 +2200,8 @@ Message editor - Enable end-to-end encryption + Enable end-to-end encryption… + You don\'t have permission to enable encryption in this room. Once enabled, encryption cannot be disabled. Enable encryption? From 3d970737d1942b6b5419561fde90c8462343c1a8 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Mon, 16 Nov 2020 14:45:42 +0000 Subject: [PATCH 19/36] Remove redundant return Signed-off-by: Dominic Fischer --- .../org/matrix/android/sdk/internal/raw/DefaultRawService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt index 5107ba5b50..3b0d7546e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt @@ -38,6 +38,6 @@ internal class DefaultRawService @Inject constructor( } override suspend fun clearCache() { - return cleanRawCacheTask.execute(Unit) + cleanRawCacheTask.execute(Unit) } } From 9216eed1b8bf50aef853bd9436e5027a0a8a8720 Mon Sep 17 00:00:00 2001 From: gradle-update-robot Date: Tue, 17 Nov 2020 00:40:54 +0000 Subject: [PATCH 20/36] Update Gradle Wrapper from 6.7 to 6.7.1. Signed-off-by: gradle-update-robot --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 99d667ccdc..cdc95ef6eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionSha256Sum=22449f5231796abd892c98b2a07c9ceebe4688d192cd2d6763f8e3bf8acbedeb +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 82b21e6a09607eebd45afbb603a97c228fa835d6 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Tue, 17 Nov 2020 10:53:57 +0000 Subject: [PATCH 21/36] Ensure draft is saved with NonCancellable Signed-off-by: Dominic Fischer --- .../app/features/home/room/detail/RoomDetailViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index ffaeb1b157..98bcdbe60e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -50,6 +50,7 @@ import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.commonmark.parser.Parser @@ -476,7 +477,7 @@ class RoomDetailViewModel @AssistedInject constructor( * Convert a send mode to a draft and save the draft */ private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState { - viewModelScope.launch { + viewModelScope.launch(NonCancellable) { when { it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { setState { copy(sendMode = it.sendMode.copy(action.draft)) } From e42cad68b4356e149735bc868c8eef89a461f880 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Fri, 13 Nov 2020 18:47:38 +0000 Subject: [PATCH 22/36] Convert Group to suspend functions Signed-off-by: Dominic Fischer --- .../org/matrix/android/sdk/api/session/group/Group.kt | 6 +----- .../sdk/internal/session/group/DefaultGroup.kt | 11 ++--------- .../app/features/grouplist/GroupListViewModel.kt | 7 +++++-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt index a4186b5a32..25c69e5025 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.group -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to interact within a group. */ @@ -28,8 +25,7 @@ interface Group { /** * This methods allows you to refresh data about this group. It will be reflected on the GroupSummary. * The SDK also takes care of refreshing group data every hour. - * @param callback : the matrix callback to be notified of success or failure * @return a Cancelable to be able to cancel requests. */ - fun fetchGroupData(callback: MatrixCallback): Cancelable + suspend fun fetchGroupData() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt index 01b57767b3..b47979775a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt @@ -16,20 +16,13 @@ package org.matrix.android.sdk.internal.session.group -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.group.Group -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultGroup(override val groupId: String, - private val taskExecutor: TaskExecutor, private val getGroupDataTask: GetGroupDataTask) : Group { - override fun fetchGroupData(callback: MatrixCallback): Cancelable { + override suspend fun fetchGroupData() { val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId)) - return getGroupDataTask.configureWith(params) { - this.callback = callback - }.executeBy(taskExecutor) + return getGroupDataTask.execute(params) } } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt index 588d939635..a17aa4dbf2 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt @@ -17,6 +17,7 @@ package im.vector.app.features.grouplist +import androidx.lifecycle.viewModelScope import arrow.core.Option import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory @@ -28,7 +29,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import io.reactivex.Observable import io.reactivex.functions.BiFunction -import org.matrix.android.sdk.api.NoOpMatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams @@ -95,7 +96,9 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state -> if (state.selectedGroup?.groupId != action.groupSummary.groupId) { // We take care of refreshing group data when selecting to be sure we get all the rooms and users - session.getGroup(action.groupSummary.groupId)?.fetchGroupData(NoOpMatrixCallback()) + viewModelScope.launch { + session.getGroup(action.groupSummary.groupId)?.fetchGroupData() + } setState { copy(selectedGroup = action.groupSummary) } } } From 94b135ae956781fef1d74aed04220e8c90c8be00 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Fri, 13 Nov 2020 18:13:13 +0000 Subject: [PATCH 23/36] Convert PushRuleService to suspend functions Signed-off-by: Dominic Fischer --- .../sdk/api/pushrules/PushRuleService.kt | 10 ++--- .../notification/DefaultPushRuleService.kt | 34 ++++---------- ...sAdvancedNotificationPreferenceFragment.kt | 45 +++++++++---------- ...rSettingsNotificationPreferenceFragment.kt | 34 +++++++------- .../troubleshoot/TestAccountSettings.kt | 19 ++++---- 5 files changed, 58 insertions(+), 84 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt index 880a7be9ac..4da1662681 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt @@ -15,11 +15,9 @@ */ package org.matrix.android.sdk.api.pushrules -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.rest.RuleSet import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.util.Cancelable interface PushRuleService { /** @@ -29,13 +27,13 @@ interface PushRuleService { fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet - fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable + suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean) - fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) - fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback): Cancelable + suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) - fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) fun addPushRuleListener(listener: PushRuleListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt index 217da269f9..f55835eb62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.notification import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleSetKey @@ -24,7 +23,6 @@ import org.matrix.android.sdk.api.pushrules.getActions import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.rest.RuleSet import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper import org.matrix.android.sdk.internal.database.model.PushRulesEntity import org.matrix.android.sdk.internal.database.query.where @@ -103,37 +101,21 @@ internal class DefaultPushRuleService @Inject constructor( ) } - override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { + override suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean) { // The rules will be updated, and will come back from the next sync response - return updatePushRuleEnableStatusTask - .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) { - this.callback = callback - } - .executeBy(taskExecutor) + return updatePushRuleEnableStatusTask.execute(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) } - override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { - return addPushRuleTask - .configureWith(AddPushRuleTask.Params(kind, pushRule)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) { + return addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule)) } - override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback): Cancelable { - return updatePushRuleActionsTask - .configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) { + return updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) } - override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { - return removePushRuleTask - .configureWith(RemovePushRuleTask.Params(kind, pushRule)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) { + return removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule)) } override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt index 67b5c03638..7e4520e4d4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -15,12 +15,13 @@ */ package im.vector.app.features.settings +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import im.vector.app.R import im.vector.app.core.preference.PushRulePreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.utils.toast -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.rest.PushRuleAndKind import javax.inject.Inject @@ -50,29 +51,25 @@ class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor() if (newRule != null) { displayLoadingView() - session.updatePushRuleActions( - ruleAndKind.kind, - preference.ruleAndKind?.pushRule ?: ruleAndKind.pushRule, - newRule, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - if (!isAdded) { - return - } - preference.setPushRule(ruleAndKind.copy(pushRule = newRule)) - hideLoadingView() - } - - override fun onFailure(failure: Throwable) { - if (!isAdded) { - return - } - hideLoadingView() - // Restore the previous value - refreshDisplay() - activity?.toast(errorFormatter.toHumanReadable(failure)) - } - }) + lifecycleScope.launch { + val result = runCatching { + session.updatePushRuleActions(ruleAndKind.kind, + preference.ruleAndKind?.pushRule ?: ruleAndKind.pushRule, + newRule) + } + if (!isAdded) { + return@launch + } + result.onSuccess { + preference.setPushRule(ruleAndKind.copy(pushRule = newRule)) + } + hideLoadingView() + result.onFailure { failure -> + // Restore the previous value + refreshDisplay() + activity?.toast(errorFormatter.toHumanReadable(failure)) + } + } } false } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt index 4bee1ac0c8..47868eed51 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt @@ -23,6 +23,7 @@ import android.media.RingtoneManager import android.net.Uri import android.os.Parcelable import android.widget.Toast +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.app.R @@ -37,6 +38,7 @@ import im.vector.app.core.utils.isIgnoringBatteryOptimizations import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.features.notifications.NotificationUtils import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds @@ -318,24 +320,22 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( .find { it.ruleId == RuleIds.RULE_ID_DISABLE_ALL } ?.let { // Trick, we must enable this room to disable notifications - pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE, - it, - !switchPref.isChecked, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - // Push rules will be updated from the sync - } + lifecycleScope.launch { + try { + pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE, + it, + !switchPref.isChecked) + // Push rules will be updated from the sync + } catch (failure: Throwable) { + if (!isAdded) { + return@launch + } - override fun onFailure(failure: Throwable) { - if (!isAdded) { - return - } - - // revert the check box - switchPref.isChecked = !switchPref.isChecked - Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show() - } - }) + // revert the check box + switchPref.isChecked = !switchPref.isChecked + Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show() + } + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt index 0c3390d0b0..b78dba07f5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestAccountSettings.kt @@ -20,7 +20,8 @@ import androidx.activity.result.ActivityResultLauncher import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleKind import javax.inject.Inject @@ -48,16 +49,12 @@ class TestAccountSettings @Inject constructor(private val stringProvider: String override fun doFix() { if (manager?.diagStatus == TestStatus.RUNNING) return // wait before all is finished - session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - manager?.retry(activityResultLauncher) - } - - override fun onFailure(failure: Throwable) { - manager?.retry(activityResultLauncher) - } - }) + GlobalScope.launch { + runCatching { + session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled) + } + manager?.retry(activityResultLauncher) + } } } status = TestStatus.FAILED From a32d7f78bb8798154c5054d8e7032d8242b2c1be Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Fri, 13 Nov 2020 19:20:01 +0000 Subject: [PATCH 24/36] Convert SearchService to suspend functions Signed-off-by: Dominic Fischer --- .../sdk/session/search/SearchMessagesTest.kt | 53 ++++++++----------- .../sdk/api/session/search/SearchService.kt | 21 +++----- .../session/search/DefaultSearchService.kt | 45 +++++++--------- 3 files changed, 47 insertions(+), 72 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 7db159cd0b..ae300c936d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -71,38 +71,27 @@ class SearchMessagesTest : InstrumentedTest { commonTestHelper.await(lock) lock = CountDownLatch(1) - aliceSession - .searchService() - .search( - searchTerm = "lore", - limit = 10, - includeProfile = true, - afterLimit = 0, - beforeLimit = 10, - orderByRecent = true, - nextBatch = null, - roomId = aliceRoomId, - callback = object : MatrixCallback { - override fun onSuccess(data: SearchResult) { - super.onSuccess(data) - assertTrue(data.results?.size == 2) - assertTrue( - data.results - ?.all { - (it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse() - }.orFalse() - ) - lock.countDown() - } - - override fun onFailure(failure: Throwable) { - super.onFailure(failure) - fail(failure.localizedMessage) - lock.countDown() - } - } - ) - lock.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS) + val data = commonTestHelper.runBlockingTest { + aliceSession + .searchService() + .search( + searchTerm = "lore", + limit = 10, + includeProfile = true, + afterLimit = 0, + beforeLimit = 10, + orderByRecent = true, + nextBatch = null, + roomId = aliceRoomId + ) + } + assertTrue(data.results?.size == 2) + assertTrue( + data.results + ?.all { + (it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse() + }.orFalse() + ) aliceTimeline.removeAllListeners() cryptoTestData.cleanUp(commonTestHelper) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt index ef2eec433f..bc1c9e5769 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.search -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to search messages in rooms. */ @@ -35,15 +32,13 @@ interface SearchService { * @param beforeLimit how many events before the result are returned. * @param afterLimit how many events after the result are returned. * @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned. - * @param callback Callback to get the search result */ - fun search(searchTerm: String, - roomId: String, - nextBatch: String?, - orderByRecent: Boolean, - limit: Int, - beforeLimit: Int, - afterLimit: Int, - includeProfile: Boolean, - callback: MatrixCallback): Cancelable + suspend fun search(searchTerm: String, + roomId: String, + nextBatch: String?, + orderByRecent: Boolean, + limit: Int, + beforeLimit: Int, + afterLimit: Int, + includeProfile: Boolean): SearchResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt index 2ba1eebe61..8033b0654d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/DefaultSearchService.kt @@ -16,40 +16,31 @@ package org.matrix.android.sdk.internal.session.search -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.search.SearchResult import org.matrix.android.sdk.api.session.search.SearchService -import org.matrix.android.sdk.api.util.Cancelable import javax.inject.Inject -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultSearchService @Inject constructor( - private val taskExecutor: TaskExecutor, private val searchTask: SearchTask ) : SearchService { - override fun search(searchTerm: String, - roomId: String, - nextBatch: String?, - orderByRecent: Boolean, - limit: Int, - beforeLimit: Int, - afterLimit: Int, - includeProfile: Boolean, - callback: MatrixCallback): Cancelable { - return searchTask - .configureWith(SearchTask.Params( - searchTerm = searchTerm, - roomId = roomId, - nextBatch = nextBatch, - orderByRecent = orderByRecent, - limit = limit, - beforeLimit = beforeLimit, - afterLimit = afterLimit, - includeProfile = includeProfile - )) { - this.callback = callback - }.executeBy(taskExecutor) + override suspend fun search(searchTerm: String, + roomId: String, + nextBatch: String?, + orderByRecent: Boolean, + limit: Int, + beforeLimit: Int, + afterLimit: Int, + includeProfile: Boolean): SearchResult { + return searchTask.execute(SearchTask.Params( + searchTerm = searchTerm, + roomId = roomId, + nextBatch = nextBatch, + orderByRecent = orderByRecent, + limit = limit, + beforeLimit = beforeLimit, + afterLimit = afterLimit, + includeProfile = includeProfile + )) } } From 822ce41b54414aab2c4b9f79a5aae991a404164e Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Wed, 18 Nov 2020 14:22:07 +0000 Subject: [PATCH 25/36] Remove redundant return Signed-off-by: Dominic Fischer --- .../matrix/android/sdk/internal/session/group/DefaultGroup.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt index b47979775a..4f610fd81b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt @@ -23,6 +23,6 @@ internal class DefaultGroup(override val groupId: String, override suspend fun fetchGroupData() { val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId)) - return getGroupDataTask.execute(params) + getGroupDataTask.execute(params) } } From 92a6e9ea5ae0ac0bbb759fec31a88d30eae396ac Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Wed, 18 Nov 2020 14:23:59 +0000 Subject: [PATCH 26/36] Remove redundant return Signed-off-by: Dominic Fischer --- .../session/notification/DefaultPushRuleService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt index f55835eb62..e00d2ff26c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt @@ -103,19 +103,19 @@ internal class DefaultPushRuleService @Inject constructor( override suspend fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean) { // The rules will be updated, and will come back from the next sync response - return updatePushRuleEnableStatusTask.execute(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) + updatePushRuleEnableStatusTask.execute(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) } override suspend fun addPushRule(kind: RuleKind, pushRule: PushRule) { - return addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule)) + addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule)) } override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) { - return updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) + updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) } override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) { - return removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule)) + removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule)) } override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) { From 796ba72bde39a298caa5bfe8a11f918c5d696529 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Wed, 18 Nov 2020 14:27:46 +0000 Subject: [PATCH 27/36] Reorder Signed-off-by: Dominic Fischer --- .../VectorSettingsAdvancedNotificationPreferenceFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt index 7e4520e4d4..8d9f8d7170 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -60,10 +60,10 @@ class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor() if (!isAdded) { return@launch } + hideLoadingView() result.onSuccess { preference.setPushRule(ruleAndKind.copy(pushRule = newRule)) } - hideLoadingView() result.onFailure { failure -> // Restore the previous value refreshDisplay() From 1359c6be1d4ca3e1da650d46d35395c0de630e24 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Wed, 18 Nov 2020 15:40:22 +0000 Subject: [PATCH 28/36] Missed a spot Signed-off-by: Dominic Fischer --- .../android/sdk/internal/session/group/GroupFactory.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt index 31450763d8..653d2a6933 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.group import org.matrix.android.sdk.api.session.group.Group import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject internal interface GroupFactory { @@ -26,14 +25,12 @@ internal interface GroupFactory { } @SessionScope -internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask, - private val taskExecutor: TaskExecutor) : +internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask) : GroupFactory { override fun create(groupId: String): Group { return DefaultGroup( groupId = groupId, - taskExecutor = taskExecutor, getGroupDataTask = getGroupDataTask ) } From 2626a761ea2aff210c62ed97e1cf45068cabbb7e Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 10 Nov 2020 09:43:54 +0100 Subject: [PATCH 29/36] EmptyRoom tile with quick actions --- .../api/session/permalinks/MatrixLinkify.kt | 16 +- .../session/room/RoomAvatarResolver.kt | 12 +- .../room/alias/GetRoomIdByAliasTask.kt | 12 +- .../membership/RoomDisplayNameResolver.kt | 32 ++-- .../room/membership/RoomMemberHelper.kt | 4 + .../src/main/res/values/strings.xml | 6 + .../home/room/detail/RoomDetailAction.kt | 8 + .../home/room/detail/RoomDetailFragment.kt | 32 +++- .../home/room/detail/RoomDetailViewEvents.kt | 7 + .../home/room/detail/RoomDetailViewModel.kt | 30 ++++ .../factory/MergedHeaderItemFactory.kt | 17 +- .../timeline/item/MergedRoomCreationItem.kt | 162 ++++++++++++++---- .../features/navigation/DefaultNavigator.kt | 4 +- .../app/features/navigation/Navigator.kt | 2 +- .../roomprofile/RoomProfileActivity.kt | 16 +- .../src/main/res/drawable/ic_add_people.xml | 10 ++ ...meline_event_merged_room_creation_stub.xml | 159 +++++++++++++++-- vector/src/main/res/values/strings.xml | 10 ++ 18 files changed, 468 insertions(+), 71 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_add_people.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt index 7f264c6228..5e9f3e1eb9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/MatrixLinkify.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.permalinks import android.text.Spannable +import org.matrix.android.sdk.api.MatrixPatterns /** * MatrixLinkify take a piece of text and turns all of the @@ -35,7 +36,7 @@ object MatrixLinkify { * I disable it because it mess up with pills, and even with pills, it does not work correctly: * The url is not correct. Ex: for @user:matrix.org, the url will be @user:matrix.org, instead of a matrix.to */ - /* + // sanity checks if (spannable.isEmpty()) { return false @@ -48,14 +49,21 @@ object MatrixLinkify { val startPos = match.range.first if (startPos == 0 || text[startPos - 1] != '/') { val endPos = match.range.last + 1 - val url = text.substring(match.range) + var url = text.substring(match.range) + if (MatrixPatterns.isUserId(url) + || MatrixPatterns.isRoomAlias(url) + || MatrixPatterns.isRoomId(url) + || MatrixPatterns.isGroupId(url) + || MatrixPatterns.isEventId(url)) { + url = PermalinkService.MATRIX_TO_URL_BASE + url + } val span = MatrixPermalinkSpan(url, callback) spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } } return hasMatch - */ - return false + +// return false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt index 90ee99a919..58633c39ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -46,11 +46,13 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId val roomMembers = RoomMemberHelper(realm, roomId) val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) - if (members.size == 1) { - res = members.firstOrNull()?.avatarUrl - } else if (members.size == 2) { - val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() - res = firstOtherMember?.avatarUrl + if (roomMembers.isDirectRoom()) { + if (members.size == 1) { + res = members.firstOrNull()?.avatarUrl + } else if (members.size == 2) { + val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() + res = firstOtherMember?.avatarUrl + } } return res } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt index 8b011980d0..ebbd3b041a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task import io.realm.Realm import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal interface GetRoomIdByAliasTask : Task> { @@ -50,9 +51,14 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor( } else if (!params.searchOnServer) { Optional.from(null) } else { - roomId = executeRequest(eventBus) { - apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) - }.roomId + roomId = try { + executeRequest(eventBus) { + apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) + }.roomId + } catch (throwable: Throwable) { + Timber.d(throwable, "## Failed to get roomId from alias") + null + } Optional.from(roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 7f3796c1ce..73ae66a5b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -93,6 +93,9 @@ internal class RoomDisplayNameResolver @Inject constructor( } } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + val invitedCount = roomSummary?.invitedMembersCount ?: 0 + val joinedCount = roomSummary?.joinedMembersCount ?: 0 + val othersTotalCount = invitedCount + joinedCount - 1 val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { roomSummary.heroes.mapNotNull { userId -> roomMembers.getLastRoomMember(userId)?.takeIf { @@ -102,22 +105,29 @@ internal class RoomDisplayNameResolver @Inject constructor( } else { activeMembers.where() .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) - .limit(3) + .limit(5) .findAll() .createSnapshot() } val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { - 0 -> stringProvider.getString(R.string.room_displayname_empty_room) - 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) - 2 -> stringProvider.getString(R.string.room_displayname_two_members, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - resolveRoomMemberName(otherMembersSubset[1], roomMembers) - ) - else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members, - roomMembers.getNumberOfJoinedMembers() - 1, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - roomMembers.getNumberOfJoinedMembers() - 1) + 0 -> { + stringProvider.getString(R.string.room_displayname_empty_room) + // TODO (was xx and yyy) ... + } + 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) + else -> { + val names = otherMembersSubset.map { + resolveRoomMemberName(it, roomMembers) ?: "" + } + if (otherMembersCount <= othersTotalCount) { + val remainingCount = invitedCount + joinedCount - names.size + (names.joinToString("${stringProvider.getString(R.string.room_displayname_separator)} ") + + " " + stringProvider.getQuantityString(R.plurals.and_n_others, remainingCount, remainingCount)) + } else { + names.dropLast(1).joinToString(", ") + " & ${names.last()}" + } + } } } return name ?: roomId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt index 7105a2cc22..c18eb0936b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt @@ -98,6 +98,10 @@ internal class RoomMemberHelper(private val realm: Realm, return getNumberOfJoinedMembers() + getNumberOfInvitedMembers() } + fun isDirectRoom() : Boolean { + return roomSummary?.isDirect ?: false + } + /** * Return all the roomMembers ids which are joined or invited to the room * diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index de30a64c32..c391a4edc8 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -180,8 +180,14 @@ %1$s and 1 other %1$s and %2$d others + + & %d other + & %d others + + , Empty room + Empty room (was %s) Initial Sync:\nImporting account… Initial Sync:\nImporting crypto diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 99adc0bf83..8891218a11 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -16,6 +16,8 @@ package im.vector.app.features.home.room.detail +import android.net.Uri +import android.view.View import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event @@ -24,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.MatrixItem sealed class RoomDetailAction : VectorViewModelAction { data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() @@ -90,4 +93,9 @@ sealed class RoomDetailAction : VectorViewModelAction { data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction() + object QuickActionInvitePeople : RoomDetailAction() + object QuickActionSetAvatar : RoomDetailAction() + data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction() + object QuickActionSetTopic : RoomDetailAction() + data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9c6c473a7f..bec84f6479 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -71,6 +71,7 @@ import com.google.android.material.textfield.TextInputEditText import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.dialogs.ConfirmationDialogBuilder +import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.withColoredButton import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup @@ -82,6 +83,7 @@ import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequests +import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -149,6 +151,7 @@ import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.reactions.EmojiReactionPickerActivity +import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData @@ -196,6 +199,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import timber.log.Timber import java.io.File import java.net.URL +import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -229,7 +233,7 @@ class RoomDetailFragment @Inject constructor( JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, AttachmentsHelper.Callback, -// RoomWidgetsBannerView.Callback, + GalleryOrCameraDialogHelper.Listener, ActiveCallView.Callback { companion object { @@ -250,6 +254,8 @@ class RoomDetailFragment @Inject constructor( private const val ircPattern = " (IRC)" } + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + private val roomDetailArgs: RoomDetailArgs by args() private val glideRequests by lazy { GlideApp.with(this) @@ -364,6 +370,12 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) + RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) + RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() + RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() + is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> + navigator.openBigImageViewer(requireActivity(), it.view, item) + } }.exhaustive } @@ -372,6 +384,24 @@ class RoomDetailFragment @Inject constructor( } } + override fun onImageReady(uri: Uri?) { + uri ?: return + roomDetailViewModel.handle( + RoomDetailAction.SetAvatarAction( + newAvatarUri = uri, + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString() + ) + ) + } + + private fun handleOpenRoomSettings() { + navigator.openRoomProfile( + requireContext(), + roomDetailArgs.roomId, + RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS + ) + } + private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) { navigator.openRoom(requireContext(), openRoom.roomId, null) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index b9e3e6b31d..d5d94a0ca5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -17,10 +17,12 @@ package im.vector.app.features.home.room.detail import android.net.Uri +import android.view.View import androidx.annotation.StringRes import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.command.Command import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import java.io.File @@ -43,6 +45,11 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents() + object OpenInvitePeople : RoomDetailViewEvents() + object OpenSetRoomAvatarDialog : RoomDetailViewEvents() + object OpenRoomSettings : RoomDetailViewEvents() + data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents() + object ShowWaitingView : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 98bcdbe60e..beaecbb898 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -277,9 +277,39 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() + RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() + is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) + RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) + is RoomDetailAction.ShowRoomAvatarFullScreen -> { + _viewEvents.post( + RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) + ) + } }.exhaustive } + private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + room.updateAvatar(action.newAvatarUri, action.newAvatarFileName, it) + } + _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) + } + } + } + + private fun handleInvitePeople() { + _viewEvents.post(RoomDetailViewEvents.OpenInvitePeople) + } + + private fun handleQuickSetAvatar() { + _viewEvents.post(RoomDetailViewEvents.OpenSetRoomAvatarDialog) + } + private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) { val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId) if (existingDmRoomId == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index e7a911ceb1..23bd041e95 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -31,10 +31,14 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_ import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_ +import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent @@ -187,6 +191,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } + val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId + ?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) } + ?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel() } + ?.let { PowerLevelsHelper(it) } + val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" val attributes = MergedRoomCreationItem.Attributes( isCollapsed = isCollapsed, mergeData = mergedData, @@ -198,13 +207,19 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde hasEncryptionEvent = hasEncryption, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, readReceiptsCallback = callback, - currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" + callback = callback, + currentUserId = currentUserId, + roomSummary = roomSummaryHolder.roomSummary, + canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, + canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, + canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false ) MergedRoomCreationItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(isCollapsed && highlighted) .attributes(attributes) + .movementMethod(createLinkMovementMethod(callback)) .also { it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index 1896a812fc..a1c0b869e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -16,11 +16,14 @@ package im.vector.app.features.home.room.detail.timeline.item +import android.text.SpannableString +import android.text.method.MovementMethod +import android.text.style.ClickableSpan import android.view.View import android.view.ViewGroup import android.widget.ImageView -import android.widget.RelativeLayout import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible @@ -28,8 +31,16 @@ import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.utils.DebouncedClickListener +import im.vector.app.core.utils.tappableMatchingText import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.tools.linkify +import me.gujun.android.span.span +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.toMatrixItem @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedRoomCreationItem : BasedMergedItem() { @@ -37,11 +48,16 @@ abstract class MergedRoomCreationItem : BasedMergedItem { - this.marginEnd = leftGuideline - } - if (attributes.isEncryptionAlgorithmSecure) { - holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) - holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) { - holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description) - } else { - holder.expandView.resources.getString(R.string.encryption_enabled_tile_description) - } - holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER - holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), - null, null, null - ) - } else { - holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled) - holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description) - holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( - ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning), - null, null, null - ) - } - } else { - holder.encryptionTile.isVisible = false - } + bindEncryptionTile(holder, data) } else { holder.avatarView.visibility = View.INVISIBLE holder.summaryView.visibility = View.GONE @@ -107,6 +96,107 @@ abstract class MergedRoomCreationItem : BasedMergedItem { + this.marginEnd = leftGuideline + } + if (attributes.isEncryptionAlgorithmSecure) { + holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled) + holder.e2eTitleDescriptionView.text = if (data?.isDirectRoom == true) { + holder.expandView.resources.getString(R.string.direct_room_encryption_enabled_tile_description) + } else { + holder.expandView.resources.getString(R.string.encryption_enabled_tile_description) + } + holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER + holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black), + null, null, null + ) + } else { + holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled) + holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description) + holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning), + null, null, null + ) + } + } else { + holder.encryptionTile.isVisible = false + } + } + + private fun bindCreationSummaryTile(holder: Holder) { + val roomSummary = attributes.roomSummary + val roomDisplayName = roomSummary?.displayName + holder.roomNameText.setTextOrHide(roomDisplayName) + val isDirect = roomSummary?.isDirect == true + val membersCount = roomSummary?.otherMemberIds?.size ?: 0 + + if (isDirect) { + holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_dm, roomSummary?.displayName ?: "") + } else if (roomDisplayName.isNullOrBlank() || roomSummary.name.isBlank()) { + holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room_no_name) + } else { + holder.roomDescriptionText.text = holder.view.resources.getString(R.string.this_is_the_beginning_of_room, roomDisplayName) + } + + val topic = roomSummary?.topic + if (topic.isNullOrBlank()) { + // do not show hint for DMs or group DMs + if (!isDirect) { + val addTopicLink = holder.view.resources.getString(R.string.add_a_topic_link_text) + val styledText = SpannableString(holder.view.resources.getString(R.string.room_created_summary_no_topic_creation_text, addTopicLink)) + holder.roomTopicText.setTextOrHide(styledText.tappableMatchingText(addTopicLink, object : ClickableSpan() { + override fun onClick(widget: View) { + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetTopic) + } + })) + } + } else { + holder.roomTopicText.setTextOrHide(span { + span(holder.view.resources.getString(R.string.topic_prefix)) { + textStyle = "bold" + } + +topic.linkify(attributes.callback) + } + ) + } + holder.roomTopicText.movementMethod = movementMethod + val roomItem = roomSummary?.toMatrixItem() + if (roomItem != null) { + holder.roomAvatarImageView.isVisible = true + attributes.avatarRenderer.render(roomItem, holder.roomAvatarImageView) + holder.roomAvatarImageView.setOnClickListener(DebouncedClickListener({ view -> + attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view)) + })) + } else { + holder.roomAvatarImageView.isVisible = false + } + + if (isDirect) { + holder.addPeopleButton.isVisible = false + } else { + holder.addPeopleButton.isVisible = true + holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ -> + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople) + })) + } + + val shouldShowSetAvatar = attributes.canChangeAvatar + && (roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) + + if (shouldShowSetAvatar && roomItem?.avatarUrl.isNullOrBlank()) { + holder.setAvatarButton.isVisible = true + holder.setAvatarButton.setOnClickListener(DebouncedClickListener({ _ -> + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar) + })) + } else { + holder.setAvatarButton.isVisible = false + } + } + class Holder : BasedMergedItem.Holder(STUB_ID) { val summaryView by bind(R.id.itemNoticeTextView) val avatarView by bind(R.id.itemNoticeAvatarView) @@ -114,6 +204,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem(R.id.itemVerificationDoneTitleTextView) val e2eTitleDescriptionView by bind(R.id.itemVerificationDoneDetailTextView) + + val roomNameText by bind(R.id.roomNameTileText) + val roomDescriptionText by bind(R.id.roomNameDescriptionText) + val roomTopicText by bind(R.id.roomNameTopicText) + val roomAvatarImageView by bind(R.id.roomAvatarImageView) + val addPeopleButton by bind(R.id.creationTileAddPeopleButton) + val setAvatarButton by bind(R.id.creationTileSetAvatarButton) } companion object { @@ -126,8 +223,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem Unit, + val callback: TimelineEventController.Callback? = null, val currentUserId: String, val hasEncryptionEvent: Boolean, - val isEncryptionAlgorithmSecure: Boolean + val isEncryptionAlgorithmSecure: Boolean, + val roomSummary: RoomSummary?, + val canChangeAvatar: Boolean = false, + val canChangeName: Boolean = false, + val canChangeTopic: Boolean = false ) : BasedMergedItem.Attributes } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 106d804cd3..2d0ca86d52 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -248,8 +248,8 @@ class DefaultNavigator @Inject constructor( context.startActivity(KeysBackupManageActivity.intent(context)) } - override fun openRoomProfile(context: Context, roomId: String) { - context.startActivity(RoomProfileActivity.newIntent(context, roomId)) + override fun openRoomProfile(context: Context, roomId: String, directAccess: Int?) { + context.startActivity(RoomProfileActivity.newIntent(context, roomId, directAccess)) } override fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) { diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 1d01a5e4f0..504fccb63a 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -78,7 +78,7 @@ interface Navigator { fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false) - fun openRoomProfile(context: Context, roomId: String) + fun openRoomProfile(context: Context, roomId: String, directAccess: Int? = null) fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 734620e378..609042ffa4 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -46,10 +46,16 @@ class RoomProfileActivity : companion object { - fun newIntent(context: Context, roomId: String): Intent { + private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS" + + const val EXTRA_DIRECT_ACCESS_ROOM_ROOT = 0 + const val EXTRA_DIRECT_ACCESS_ROOM_SETTINGS = 1 + + fun newIntent(context: Context, roomId: String, directAccess: Int?): Intent { val roomProfileArgs = RoomProfileArgs(roomId) return Intent(context, RoomProfileActivity::class.java).apply { putExtra(MvRx.KEY_ARG, roomProfileArgs) + putExtra(EXTRA_DIRECT_ACCESS, directAccess) } } } @@ -80,7 +86,13 @@ class RoomProfileActivity : sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return if (isFirstCreation()) { - addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + when (intent?.extras?.getInt(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOM_ROOT)) { + EXTRA_DIRECT_ACCESS_ROOM_SETTINGS -> { + addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs) + } + else -> addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + } } sharedActionViewModel .observe() diff --git a/vector/src/main/res/drawable/ic_add_people.xml b/vector/src/main/res/drawable/ic_add_people.xml new file mode 100644 index 0000000000..3ec60095ff --- /dev/null +++ b/vector/src/main/res/drawable/ic_add_people.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml index eefa387254..6fdd30b9e2 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml @@ -1,31 +1,168 @@ - + android:layout_marginTop="0dp"> - + - + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_below="@+id/creationTile" + android:layout_marginTop="8dp"> You created and configured the room. %s joined. You joined. + This is the beginning of %s. + This is the beginning of this conversation. + This is the beginning of your direct message history with %s. + + %s to let people know what this room is about. + Add a topic + "Topic: " Almost there! Is the other device showing the same shield? Almost there! Waiting for confirmation… @@ -2511,6 +2518,7 @@ "We couldn't create your DM. Please check the users you want to invite and try again." Add members + Add people INVITE Inviting users… Invite Users @@ -2575,6 +2583,8 @@ Room Name Topic You changed room settings successfully + Set avatar + You cannot access this message Waiting for this message, this may take a while From 264bc52bcc2d8844765201892c9869e7ed9d5cbd Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 13 Nov 2020 09:27:34 +0100 Subject: [PATCH 30/36] WIP review --- .../session/room/alias/GetRoomIdByAliasTask.kt | 15 ++++++--------- .../timeline/item/MergedRoomCreationItem.kt | 13 +++++++------ .../RoomMemberProfileController.kt | 2 +- ...m_timeline_event_merged_room_creation_stub.xml | 2 +- vector/src/main/res/values/strings.xml | 2 +- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt index ebbd3b041a..58a119cc77 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -17,6 +17,9 @@ package org.matrix.android.sdk.internal.session.room.alias import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.findByAlias @@ -24,9 +27,6 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task -import io.realm.Realm -import org.greenrobot.eventbus.EventBus -import timber.log.Timber import javax.inject.Inject internal interface GetRoomIdByAliasTask : Task> { @@ -51,14 +51,11 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor( } else if (!params.searchOnServer) { Optional.from(null) } else { - roomId = try { + roomId = tryOrNull("## Failed to get roomId from alias") { executeRequest(eventBus) { apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) - }.roomId - } catch (throwable: Throwable) { - Timber.d(throwable, "## Failed to get roomId from alias") - null - } + } + }?.roomId Optional.from(roomId) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index a1c0b869e2..dcdb2bab29 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -155,12 +155,13 @@ abstract class MergedRoomCreationItem : BasedMergedItem + tools:text="@string/room_created_summary_no_topic_creation_text" /> This is the beginning of this conversation. This is the beginning of your direct message history with %s. - %s to let people know what this room is about. + %s to let people know what this room is about. Add a topic "Topic: " From 1de5cd2e61b53d00c0289bff5f7333962c97c239 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 16 Nov 2020 09:56:26 +0100 Subject: [PATCH 31/36] Code review --- .../session/room/RoomAvatarResolver.kt | 5 ++- .../membership/RoomDisplayNameResolver.kt | 38 ++++++++++++++----- .../room/membership/RoomMemberHelper.kt | 8 +--- .../src/main/res/values/strings.xml | 8 ++-- .../RoomMemberProfileController.kt | 2 +- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt index 58633c39ba..99f9d3644d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -26,6 +26,8 @@ import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import io.realm.Realm +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where import javax.inject.Inject internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { @@ -46,7 +48,8 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId val roomMembers = RoomMemberHelper(realm, roomId) val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) - if (roomMembers.isDirectRoom()) { + val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect ?: false + if (isDirectRoom) { if (members.size == 1) { res = members.firstOrNull()?.avatarUrl } else if (members.size == 2) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 73ae66a5b2..8d0789d675 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -116,17 +116,35 @@ internal class RoomDisplayNameResolver @Inject constructor( // TODO (was xx and yyy) ... } 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) + 2 -> { + stringProvider.getString(R.string.room_displayname_two_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) + ) + } + 3 -> { + stringProvider.getString(R.string.room_displayname_3_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers), + resolveRoomMemberName(otherMembersSubset[2], roomMembers) + ) + } + 4 -> { + stringProvider.getString(R.string.room_displayname_4_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers), + resolveRoomMemberName(otherMembersSubset[2], roomMembers), + resolveRoomMemberName(otherMembersSubset[3], roomMembers) + ) + } else -> { - val names = otherMembersSubset.map { - resolveRoomMemberName(it, roomMembers) ?: "" - } - if (otherMembersCount <= othersTotalCount) { - val remainingCount = invitedCount + joinedCount - names.size - (names.joinToString("${stringProvider.getString(R.string.room_displayname_separator)} ") - + " " + stringProvider.getQuantityString(R.plurals.and_n_others, remainingCount, remainingCount)) - } else { - names.dropLast(1).joinToString(", ") + " & ${names.last()}" - } + val remainingCount = invitedCount + joinedCount - otherMembersCount + 1 + stringProvider.getString(R.string.room_displayname_four_and_more_members, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers), + resolveRoomMemberName(otherMembersSubset[2], roomMembers), + remainingCount + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt index c18eb0936b..2a7c46bd42 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberHelper.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.session.room.membership +import io.realm.Realm +import io.realm.RealmQuery import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity @@ -25,8 +27,6 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where -import io.realm.Realm -import io.realm.RealmQuery /** * This class is an helper around STATE_ROOM_MEMBER events. @@ -98,10 +98,6 @@ internal class RoomMemberHelper(private val realm: Realm, return getNumberOfJoinedMembers() + getNumberOfInvitedMembers() } - fun isDirectRoom() : Boolean { - return roomSummary?.isDirect ?: false - } - /** * Return all the roomMembers ids which are joined or invited to the room * diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index c391a4edc8..130ad5570c 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -175,16 +175,14 @@ %1$s and %2$s + %1$s, %2$s and %3$s + %1$s, %2$s, %3$s and %4$s + %1$s, %2$s, %3$s and %4$d others %1$s and 1 other %1$s and %2$d others - - & %d other - & %d others - - , Empty room Empty room (was %s) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index 44e726735f..2e91091443 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -161,7 +161,7 @@ class RoomMemberProfileController @Inject constructor( } else { genericFooterItem { id("verify_footer_not_encrypted") - text(RRstringProvider.getString(R.string.room_profile_not_encrypted_subtitle)) + text(stringProvider.getString(R.string.room_profile_not_encrypted_subtitle)) centered(false) } } From 206e68b1d202a30523a59eaddd2081b8a24cf3d0 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 18 Nov 2020 10:17:50 +0100 Subject: [PATCH 32/36] Unused val --- .../internal/session/room/membership/RoomDisplayNameResolver.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 8d0789d675..c69773fbeb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -95,7 +95,6 @@ internal class RoomDisplayNameResolver @Inject constructor( val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val invitedCount = roomSummary?.invitedMembersCount ?: 0 val joinedCount = roomSummary?.joinedMembersCount ?: 0 - val othersTotalCount = invitedCount + joinedCount - 1 val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { roomSummary.heroes.mapNotNull { userId -> roomMembers.getLastRoomMember(userId)?.takeIf { From 1eac90e5b153879d9a2a16dba35ba98a87933699 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Nov 2020 16:07:32 +0100 Subject: [PATCH 33/36] Use plurals for proper i18n --- .../room/membership/RoomDisplayNameResolver.kt | 14 ++++++++------ matrix-sdk-android/src/main/res/values/strings.xml | 9 +++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index c69773fbeb..a7dfcfc96f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -110,25 +110,25 @@ internal class RoomDisplayNameResolver @Inject constructor( } val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { - 0 -> { + 0 -> { stringProvider.getString(R.string.room_displayname_empty_room) // TODO (was xx and yyy) ... } - 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) - 2 -> { + 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) + 2 -> { stringProvider.getString(R.string.room_displayname_two_members, resolveRoomMemberName(otherMembersSubset[0], roomMembers), resolveRoomMemberName(otherMembersSubset[1], roomMembers) ) } - 3 -> { + 3 -> { stringProvider.getString(R.string.room_displayname_3_members, resolveRoomMemberName(otherMembersSubset[0], roomMembers), resolveRoomMemberName(otherMembersSubset[1], roomMembers), resolveRoomMemberName(otherMembersSubset[2], roomMembers) ) } - 4 -> { + 4 -> { stringProvider.getString(R.string.room_displayname_4_members, resolveRoomMemberName(otherMembersSubset[0], roomMembers), resolveRoomMemberName(otherMembersSubset[1], roomMembers), @@ -138,7 +138,9 @@ internal class RoomDisplayNameResolver @Inject constructor( } else -> { val remainingCount = invitedCount + joinedCount - otherMembersCount + 1 - stringProvider.getString(R.string.room_displayname_four_and_more_members, + stringProvider.getQuantityString( + R.plurals.room_displayname_four_and_more_members, + remainingCount, resolveRoomMemberName(otherMembersSubset[0], roomMembers), resolveRoomMemberName(otherMembersSubset[1], roomMembers), resolveRoomMemberName(otherMembersSubset[2], roomMembers), diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 130ad5570c..f77cd3203d 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -175,10 +175,15 @@ %1$s and %2$s + %1$s, %2$s and %3$s + %1$s, %2$s, %3$s and %4$s - %1$s, %2$s, %3$s and %4$d others - + + + %1$s, %2$s, %3$s and %4$d other + %1$s, %2$s, %3$s and %4$d others + %1$s and 1 other %1$s and %2$d others From b82b378cfe386e394874a2a4ea46f4cf069ba1b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Nov 2020 17:00:44 +0100 Subject: [PATCH 34/36] Cleanup and changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 936e6b0ffe..e98329d91f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ Features ✨: - Improvements 🙌: + - New room creation tile with quick action (#2346) - Open an existing DM instead of creating a new one (#2319) - Ask for explicit user consent to send their contact details to the identity server (#2375) - Handle events of type "m.room.server_acl" (#890) From 9ed8f26d7c78301ae1b619ab6c25e51a80e77d0c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Nov 2020 17:15:36 +0100 Subject: [PATCH 35/36] Make the room default avatar clickable to set it (as per the small picto) And do some cleanup --- .../timeline/item/MergedRoomCreationItem.kt | 43 ++++++++++--------- ...meline_event_merged_room_creation_stub.xml | 28 +++++------- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index dcdb2bab29..34b9ae1b9d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -165,36 +165,37 @@ abstract class MergedRoomCreationItem : BasedMergedItem= 2)) + && roomItem?.avatarUrl.isNullOrBlank() + + holder.roomAvatarImageView.isVisible = roomItem != null if (roomItem != null) { - holder.roomAvatarImageView.isVisible = true attributes.avatarRenderer.render(roomItem, holder.roomAvatarImageView) holder.roomAvatarImageView.setOnClickListener(DebouncedClickListener({ view -> - attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view)) - })) - } else { - holder.roomAvatarImageView.isVisible = false - } - - if (isDirect) { - holder.addPeopleButton.isVisible = false - } else { - holder.addPeopleButton.isVisible = true - holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ -> - attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople) + if (shouldSetAvatar) { + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar) + } else { + // Note: this is no op if there is no avatar on the room + attributes.callback?.onTimelineItemAction(RoomDetailAction.ShowRoomAvatarFullScreen(roomItem, view)) + } })) } - val shouldShowSetAvatar = attributes.canChangeAvatar - && (roomSummary?.isDirect == false || (isDirect && membersCount >= 2)) - - if (shouldShowSetAvatar && roomItem?.avatarUrl.isNullOrBlank()) { - holder.setAvatarButton.isVisible = true + holder.setAvatarButton.isVisible = shouldSetAvatar + if (shouldSetAvatar) { holder.setAvatarButton.setOnClickListener(DebouncedClickListener({ _ -> attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionSetAvatar) })) - } else { - holder.setAvatarButton.isVisible = false + } + + holder.addPeopleButton.isVisible = !isDirect + if (!isDirect) { + holder.addPeopleButton.setOnClickListener(DebouncedClickListener({ _ -> + attributes.callback?.onTimelineItemAction(RoomDetailAction.QuickActionInvitePeople) + })) } } @@ -209,7 +210,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem(R.id.roomNameTileText) val roomDescriptionText by bind(R.id.roomNameDescriptionText) val roomTopicText by bind(R.id.roomNameTopicText) - val roomAvatarImageView by bind(R.id.roomAvatarImageView) + val roomAvatarImageView by bind(R.id.creationTileRoomAvatarImageView) val addPeopleButton by bind(R.id.creationTileAddPeopleButton) val setAvatarButton by bind(R.id.creationTileSetAvatarButton) } diff --git a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml index 8dba361f5e..ccb9bacd30 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml @@ -3,16 +3,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> - + android:layout_height="wrap_content"> + android:layout_height="wrap_content"> + @@ -74,8 +71,8 @@ android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/roomAvatarImageView" - tools:text="Room Name" /> + app:layout_constraintTop_toBottomOf="@id/creationTileRoomAvatarImageView" + tools:text="@sample/matrix.json/data/roomName" /> - + tools:text="@string/this_is_the_beginning_of_room_no_name" /> - + app:layout_constraintTop_toTopOf="@id/addPeopleButtonBg" /> + From c29e4648ea6a8a1a8d2bb3553f43b74103c7bd2d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Nov 2020 17:34:51 +0100 Subject: [PATCH 36/36] Small cleanup --- ...meline_event_merged_room_creation_stub.xml | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml index ccb9bacd30..728b90b696 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml @@ -102,7 +102,7 @@ app:layout_constraintTop_toBottomOf="@id/roomNameDescriptionText" tools:text="@string/room_created_summary_no_topic_creation_text" /> - - - - + android:importantForAccessibility="no" + android:scaleType="center" + android:src="@drawable/ic_add_people" /> + android:textColor="@color/riotx_accent" /> - +