From eb1fa0919f410ea549370d3bfd8d3eb07534f71b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Nov 2020 18:08:46 +0100 Subject: [PATCH 01/48] 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 51a95554d7b4043e364a986ffe1fb815ad7379fa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 5 Nov 2020 18:30:00 +0100 Subject: [PATCH 02/48] Remove useless code. We can now inject element in the Fragment constructors --- .../java/im/vector/app/core/platform/VectorBaseFragment.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 179e21a6d8..cd38f5aeaa 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -97,12 +97,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog() viewModelFactory = screenComponent.viewModelFactory() childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() - injectWith(injector()) super.onAttach(context) } - protected open fun injectWith(injector: ScreenComponent) = Unit - @CallSuper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From 6dfdc77ebd0ef07f34f62c6cb0f6dd5b9ab6e0ee Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 6 Nov 2020 14:41:43 +0100 Subject: [PATCH 03/48] 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 62ca9a2a848e3b1411d595dfb5d196b3ec10a26d Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sun, 8 Nov 2020 14:37:02 +0000 Subject: [PATCH 04/48] Fix misleading identifier Signed-off-by: Dominic Fischer --- .../app/features/home/room/detail/RoomDetailViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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..81eedcef5f 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 @@ -164,7 +164,7 @@ class RoomDetailViewModel @AssistedInject constructor( getUnreadState() observeSyncState() observeEventDisplayedActions() - getDraftIfAny() + loadDraftIfAny() observeUnreadState() observeMyRoomMember() observeActiveRoomWidgets() @@ -495,7 +495,7 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun getDraftIfAny() { + private fun loadDraftIfAny() { val currentDraft = room.getDraft() ?: return setState { copy( @@ -772,7 +772,7 @@ class RoomDetailViewModel @AssistedInject constructor( private fun popDraft() = withState { if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { // If we were sharing, we want to get back our last value from draft - getDraftIfAny() + loadDraftIfAny() } else { // Otherwise we clear the composer and remove the draft from db setState { copy(sendMode = SendMode.REGULAR("", false)) } From dee6f358880554b9c3795f087947120d506a07fb Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sun, 8 Nov 2020 14:57:15 +0000 Subject: [PATCH 05/48] Fix draft bug 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 81eedcef5f..102a0673d4 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 @@ -496,7 +496,7 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun loadDraftIfAny() { - val currentDraft = room.getDraft() ?: return + val currentDraft = room.getDraft() setState { copy( // Create a sendMode from a draft and retrieve the TimelineEvent @@ -517,6 +517,7 @@ class RoomDetailViewModel @AssistedInject constructor( SendMode.EDIT(timelineEvent, currentDraft.text) } } + else -> null } ?: SendMode.REGULAR("", fromSharing = false) ) } From c34750bdabdd607c17bfb95b25b6101eecbf626a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Nov 2020 16:49:31 +0100 Subject: [PATCH 06/48] Add Onuray in the core team authors --- AUTHORS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index 4fb5b8c994..f6ff724fec 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -26,6 +26,11 @@ Even if we try to be able to work on all the functionalities, we have more knowl - Product manager, Android developer - Specialist on the crypto implementation. +## Onuray: Android developer + +[@onurays:matrix.org](https://matrix.to/#/@onurays:matrix.org) +- Android developer + # Other contributors First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function. From 16448c0bc5eac8ccfe5520fa33ff69962e200edd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Nov 2020 16:53:17 +0100 Subject: [PATCH 07/48] Add Github link to the authors --- AUTHORS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index f6ff724fec..823dbc7311 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,7 +4,7 @@ A full developer contributors list can be found [here](https://github.com/vector Even if we try to be able to work on all the functionalities, we have more knowledge about what we have developed ourselves. -## Benoit: Android team leader +## [Benoit](https://github.com/bmarty): Android team leader [@benoit.marty:matrix.org](https://matrix.to/#/@benoit.marty:matrix.org) - Android team leader and project leader, Android developer, GitHub community manager. @@ -12,7 +12,7 @@ Even if we try to be able to work on all the functionalities, we have more knowl - Reviewing and polishing developed features, code quality manager, PRs reviewer, GitHub community manager. - Release manager on the Play Store -## François: Software architect +## [Ganfra](https://github.com/ganfra) (aka François): Software architect [@ganfra:matrix.org](https://matrix.to/#/@ganfra:matrix.org) - Software architect, Android developer @@ -20,13 +20,13 @@ Even if we try to be able to work on all the functionalities, we have more knowl - Work mainly on the global architecture of the project. - Specialist of the timeline, and lots of other features. -## Valere: Product manager, Android developer +## [Valere](https://github.com/BillCarsonFr): Product manager, Android developer [@valere35:matrix.org](https://matrix.to/#/@valere35:matrix.org) - Product manager, Android developer - Specialist on the crypto implementation. -## Onuray: Android developer +## [Onuray](https://github.com/onurays): Android developer [@onurays:matrix.org](https://matrix.to/#/@onurays:matrix.org) - Android developer From be3bc175bf62ff1edcf95ed256e4f483ea302b69 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Mon, 9 Nov 2020 18:23:45 +0000 Subject: [PATCH 08/48] Update CHANGES.md Signed-off-by: Dominic Fischer --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index fa30acabd5..87ea4c031d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Improvements 🙌: Bugfix 🐛: - Fix issue when updating the avatar of a room + - Fix issue when restoring draft after sharing (#2287) Translations 🗣: - From a2a2015af6ee190b578a4a6a7092a73a52f0e24f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 10 Nov 2020 17:17:05 +0300 Subject: [PATCH 09/48] 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 10/48] 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 3ce8deec07512cfeb813cbf624f617d82c1467b2 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sun, 8 Nov 2020 12:24:47 +0000 Subject: [PATCH 11/48] Convert RoomCryptoService to suspend functions Signed-off-by: Dominic Fischer --- .../android/sdk/common/CryptoTestHelper.kt | 4 ++-- .../api/session/room/crypto/RoomCryptoService.kt | 4 +--- .../sdk/internal/session/room/DefaultRoom.kt | 12 ++++-------- .../settings/RoomSettingsViewModel.kt | 16 +++++++--------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 1a9165ade4..cbb22daf0f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -68,8 +68,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { if (encryptedRoom) { val room = aliceSession.getRoom(roomId)!! - mTestHelper.doSync { - room.enableEncryption(callback = it) + mTestHelper.runBlockingTest { + room.enableEncryption() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt index e7e6bacc22..1251fd9857 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.room.crypto -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM interface RoomCryptoService { @@ -30,6 +29,5 @@ interface RoomCryptoService { /** * Enable encryption of the room */ - fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, - callback: MatrixCallback) + suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 1338df6878..c7bb640f7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -101,13 +101,13 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, return cryptoService.shouldEncryptForInvitedMembers(roomId) } - override fun enableEncryption(algorithm: String, callback: MatrixCallback) { + override suspend fun enableEncryption(algorithm: String) { when { isEncrypted() -> { - callback.onFailure(IllegalStateException("Encryption is already enabled for this room")) + throw IllegalStateException("Encryption is already enabled for this room") } algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> { - callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")) + throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported") } else -> { val params = SendStateTask.Params( @@ -118,11 +118,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, "algorithm" to algorithm )) - sendStateTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + sendStateTask.execute(params) } } } 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 4e540f867e..086ce93bb0 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,6 +17,7 @@ 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 @@ -27,7 +28,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Completable import io.reactivex.Observable -import org.matrix.android.sdk.api.MatrixCallback +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 @@ -228,16 +229,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: private fun handleEnableEncryption() { postLoading(true) - room.enableEncryption(callback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - postLoading(false) + viewModelScope.launch { + val result = runCatching { room.enableEncryption() } + postLoading(false) + result.onFailure { failure -> _viewEvents.post(RoomSettingsViewEvents.Failure(failure)) } - - override fun onSuccess(data: Unit) { - postLoading(false) - } - }) + } } private fun postLoading(isLoading: Boolean) { From ccf5d759a4c4a442d1045b48aa7cf05a850f2519 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Nov 2020 15:12:49 +0100 Subject: [PATCH 12/48] 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 13/48] 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 14/48] 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 15/48] 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 16/48] 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 413a55623e1b6ee9071f4515513fe04f47fb7eaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 00:39:16 +0100 Subject: [PATCH 17/48] Handle events of type "m.room.server_acl" (#890) --- CHANGES.md | 1 + .../android/sdk/api/extensions/Strings.kt | 5 ++ .../sdk/api/session/events/model/EventType.kt | 1 + .../room/model/RoomServerAclContent.kt | 59 +++++++++++++++ .../src/main/res/values/strings.xml | 17 +++++ .../action/MessageActionsViewModel.kt | 1 + .../timeline/factory/TimelineItemFactory.kt | 1 + .../timeline/format/NoticeEventFormatter.kt | 74 +++++++++++++++++++ .../helper/TimelineDisplayableEvents.kt | 1 + 9 files changed, 160 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomServerAclContent.kt diff --git a/CHANGES.md b/CHANGES.md index 4a6133c103..9387ae161e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Open an existing DM instead of creating a new one (#2319) + - Handle events of type "m.room.server_acl" (#890) Bugfix 🐛: - Fix issue when restoring draft after sharing (#2287) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt index a17e65b8e0..e264843ea4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt @@ -22,3 +22,8 @@ fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { else -> "$prefix$this" } } + +/** + * Append a new line and then the provided string + */ +fun StringBuilder.appendNl(str: String) = append("\n").append(str) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 82dea81a5b..0a7f3ff09f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -56,6 +56,7 @@ object EventType { const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups" const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events" const val STATE_ROOM_ENCRYPTION = "m.room.encryption" + const val STATE_ROOM_SERVER_ACL = "m.room.server_acl" // Call Events const val CALL_INVITE = "m.call.invite" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomServerAclContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomServerAclContent.kt new file mode 100644 index 0000000000..92078054b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomServerAclContent.kt @@ -0,0 +1,59 @@ +/* + * 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.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_SERVER_ACL state event content + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-server-acl + */ +@JsonClass(generateAdapter = true) +data class RoomServerAclContent( + /** + * True to allow server names that are IP address literals. False to deny. + * Defaults to true if missing or otherwise not a boolean. + * This is strongly recommended to be set to false as servers running with IP literal names are strongly + * discouraged in order to require legitimate homeservers to be backed by a valid registered domain name. + */ + @Json(name = "allow_ip_literals") + val allowIpLiterals: Boolean = true, + + /** + * The server names to allow in the room, excluding any port information. Wildcards may be used to cover + * a wider range of hosts, where * matches zero or more characters and ? matches exactly one character. + * + * This defaults to an empty list when not provided, effectively disallowing every server. + */ + @Json(name = "allow") + val allowList: List = emptyList(), + + /** + * The server names to disallow in the room, excluding any port information. Wildcards may be used to cover + * a wider range of hosts, where * matches zero or more characters and ? matches exactly one character. + * + * This defaults to an empty list when not provided. + */ + @Json(name = "deny") + val denyList: List = emptyList() + +) { + companion object { + const val ALL = "*" + } +} diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 27f083269f..023650e0af 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -72,6 +72,23 @@ You upgraded this room. %s upgraded here. You upgraded here. + %s set the server ACLs for this room: + You set the server ACLs for this room: + • Server matching %s are banned. + • Server matching %s are allowed. + • Server matching IP literals are allowed. + • Server matching IP literals are banned. + + %s changed the server ACLs for this room: + You changed the server ACLs for this room: + • Server matching %s are now banned. + • Server matching %s were removed from the ban list. + • Server matching %s are now allowed. + • Server matching %s were removed from the allowed list. + • Server matching IP literals are now allowed. + • Server matching IP literals are now banned. + No change. + 🎉 All servers are banned from participating! This room can no longer be used. %1$s requested a VoIP conference You requested a VoIP conference 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..8b0b905805 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 @@ -186,6 +186,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_SERVER_ACL, EventType.CALL_INVITE, EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 1a4db3bdfc..575f28b610 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -57,6 +57,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 8055ef9a99..c93f6b9837 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.format import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.extensions.appendNl import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -35,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.api.session.room.model.RoomServerAclContent import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent @@ -72,6 +74,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) + EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_WIDGET, @@ -383,6 +386,77 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour } } + private fun formatRoomServerAclEvent(event: Event, senderName: String?): String? { + val eventContent = event.getClearContent().toModel() ?: return null + val prevEventContent = event.resolvedPrevContent()?.toModel() + + return buildString { + // Title + append(if (prevEventContent == null) { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_server_acl_set_title_by_you) + } else { + sp.getString(R.string.notice_room_server_acl_set_title, senderName) + } + } else { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_server_acl_updated_title_by_you) + } else { + sp.getString(R.string.notice_room_server_acl_updated_title, senderName) + } + }) + // Details + if (eventContent.allowList.isEmpty()) { + // Special case for stuck room + append("\n") + append(sp.getString(R.string.notice_room_server_acl_allow_is_empty)) + } else { + if (prevEventContent == null) { + eventContent.allowList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_allowed, it)) } + eventContent.denyList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_banned, it)) } + if (eventContent.allowIpLiterals) { + appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_allowed)) + } else { + appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_not_allowed)) + } + } else { + // Display only diff + var hasChanged = false + // New allowed servers + (eventContent.allowList - prevEventContent.allowList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_allowed, it)) } + // Removed allowed servers + (prevEventContent.allowList - eventContent.allowList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_allowed, it)) } + // New denied servers + (eventContent.denyList - prevEventContent.denyList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_banned, it)) } + // Removed denied servers + (prevEventContent.denyList - eventContent.denyList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_banned, it)) } + + + if (prevEventContent.allowIpLiterals != eventContent.allowIpLiterals) { + hasChanged = true + if (eventContent.allowIpLiterals) { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_allowed)) + } else { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_not_allowed)) + } + } + + if (!hasChanged) { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_no_change)) + } + } + } + } + } + private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? { val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel() val canonicalAlias = eventContent?.canonicalAlias diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 14b8c12fee..4fcac6c7f7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -33,6 +33,7 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_POWER_LEVELS, EventType.CALL_INVITE, EventType.CALL_HANGUP, From 60ce351a27c95cc9e0b2e5aeb71345458273ac49 Mon Sep 17 00:00:00 2001 From: Dominic Fischer Date: Sun, 8 Nov 2020 12:44:46 +0000 Subject: [PATCH 18/48] Convert RoomPushRuleService to suspend functions Signed-off-by: Dominic Fischer --- .../room/notification/RoomPushRuleService.kt | 4 +--- .../notification/DefaultRoomPushRuleService.kt | 15 +++------------ .../features/home/room/list/RoomListViewModel.kt | 14 ++++++++++---- .../features/roomprofile/RoomProfileViewModel.kt | 8 +++++--- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt index 32d6033578..eb822c68ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/notification/RoomPushRuleService.kt @@ -17,12 +17,10 @@ package org.matrix.android.sdk.api.session.room.notification import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable interface RoomPushRuleService { fun getLiveRoomNotificationState(): LiveData - fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable + suspend fun setRoomNotificationState(roomNotificationState: RoomNotificationState) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt index 8797b0c764..67ae55c066 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt @@ -21,21 +21,16 @@ import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.model.PushRuleEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted private val roomId: String, private val setRoomNotificationStateTask: SetRoomNotificationStateTask, - @SessionDatabase private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor) + @SessionDatabase private val monarchy: Monarchy) : RoomPushRuleService { @AssistedInject.Factory @@ -49,12 +44,8 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted } } - override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable { - return setRoomNotificationStateTask - .configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) { - this.callback = matrixCallback - } - .executeBy(taskExecutor) + override suspend fun setRoomNotificationState(roomNotificationState: RoomNotificationState) { + setRoomNotificationStateTask.execute(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) } private fun getPushRuleForRoom(): LiveData { 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 c32629d6ae..b3af3b5e95 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 @@ -36,6 +36,7 @@ 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 import javax.inject.Inject class RoomListViewModel @Inject constructor(initialState: RoomListViewState, @@ -169,11 +170,16 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) { - session.getRoom(action.roomId)?.setRoomNotificationState(action.notificationState, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomListViewEvents.Failure(failure)) + val room = session.getRoom(action.roomId) + if (room != null) { + viewModelScope.launch { + try { + room.setRoomNotificationState(action.notificationState) + } catch (failure: Exception) { + _viewEvents.post(RoomListViewEvents.Failure(failure)) + } } - }) + } } private fun handleToggleTag(action: RoomListAction.ToggleTag) { 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 e927ec9876..a78bf472d6 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 @@ -102,11 +102,13 @@ class RoomProfileViewModel @AssistedInject constructor( } private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) { - room.setRoomNotificationState(action.notificationState, object : MatrixCallback { - override fun onFailure(failure: Throwable) { + viewModelScope.launch { + try { + room.setRoomNotificationState(action.notificationState) + } catch (failure: Throwable) { _viewEvents.post(RoomProfileViewEvents.Failure(failure)) } - }) + } } private fun handleLeaveRoom() { From b99cdf736703f1aa65c4ad7e462bd9c0baff1ce7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 11:07:50 +0100 Subject: [PATCH 19/48] Handle events of type "m.room.server_acl" - details only in developer mode (#890) --- .../src/main/res/values/strings.xml | 8 +- .../timeline/format/NoticeEventFormatter.kt | 99 ++++++++++--------- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 023650e0af..de30a64c32 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -72,15 +72,15 @@ You upgraded this room. %s upgraded here. You upgraded here. - %s set the server ACLs for this room: - You set the server ACLs for this room: + %s set the server ACLs for this room. + You set the server ACLs for this room. • Server matching %s are banned. • Server matching %s are allowed. • Server matching IP literals are allowed. • Server matching IP literals are banned. - %s changed the server ACLs for this room: - You changed the server ACLs for this room: + %s changed the server ACLs for this room. + You changed the server ACLs for this room. • Server matching %s are now banned. • Server matching %s were removed from the ban list. • Server matching %s are now allowed. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index c93f6b9837..b5859ba1ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.format import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.extensions.appendNl import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event @@ -50,9 +51,12 @@ import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import timber.log.Timber import javax.inject.Inject -class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource, - private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, - private val sp: StringProvider) { +class NoticeEventFormatter @Inject constructor( + private val activeSessionDataSource: ActiveSessionDataSource, + private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, + private val vectorPreferences: VectorPreferences, + private val sp: StringProvider +) { private val currentUserId: String? get() = activeSessionDataSource.currentValue?.orNull()?.myUserId @@ -405,55 +409,58 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour sp.getString(R.string.notice_room_server_acl_updated_title, senderName) } }) - // Details if (eventContent.allowList.isEmpty()) { // Special case for stuck room - append("\n") - append(sp.getString(R.string.notice_room_server_acl_allow_is_empty)) + appendNl(sp.getString(R.string.notice_room_server_acl_allow_is_empty)) + } else if (vectorPreferences.developerMode()) { + // Details, only in developer mode + appendAclDetails(eventContent, prevEventContent) + } + } + } + + private fun StringBuilder.appendAclDetails(eventContent: RoomServerAclContent, prevEventContent: RoomServerAclContent?) { + if (prevEventContent == null) { + eventContent.allowList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_allowed, it)) } + eventContent.denyList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_banned, it)) } + if (eventContent.allowIpLiterals) { + appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_allowed)) } else { - if (prevEventContent == null) { - eventContent.allowList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_allowed, it)) } - eventContent.denyList.forEach { appendNl(sp.getString(R.string.notice_room_server_acl_set_banned, it)) } - if (eventContent.allowIpLiterals) { - appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_allowed)) - } else { - appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_not_allowed)) - } + appendNl(sp.getString(R.string.notice_room_server_acl_set_ip_literals_not_allowed)) + } + } else { + // Display only diff + var hasChanged = false + // New allowed servers + (eventContent.allowList - prevEventContent.allowList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_allowed, it)) } + // Removed allowed servers + (prevEventContent.allowList - eventContent.allowList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_allowed, it)) } + // New denied servers + (eventContent.denyList - prevEventContent.denyList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_banned, it)) } + // Removed denied servers + (prevEventContent.denyList - eventContent.denyList) + .also { hasChanged = hasChanged || it.isNotEmpty() } + .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_banned, it)) } + + + if (prevEventContent.allowIpLiterals != eventContent.allowIpLiterals) { + hasChanged = true + if (eventContent.allowIpLiterals) { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_allowed)) } else { - // Display only diff - var hasChanged = false - // New allowed servers - (eventContent.allowList - prevEventContent.allowList) - .also { hasChanged = hasChanged || it.isNotEmpty() } - .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_allowed, it)) } - // Removed allowed servers - (prevEventContent.allowList - eventContent.allowList) - .also { hasChanged = hasChanged || it.isNotEmpty() } - .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_allowed, it)) } - // New denied servers - (eventContent.denyList - prevEventContent.denyList) - .also { hasChanged = hasChanged || it.isNotEmpty() } - .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_banned, it)) } - // Removed denied servers - (prevEventContent.denyList - eventContent.denyList) - .also { hasChanged = hasChanged || it.isNotEmpty() } - .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_banned, it)) } - - - if (prevEventContent.allowIpLiterals != eventContent.allowIpLiterals) { - hasChanged = true - if (eventContent.allowIpLiterals) { - appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_allowed)) - } else { - appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_not_allowed)) - } - } - - if (!hasChanged) { - appendNl(sp.getString(R.string.notice_room_server_acl_updated_no_change)) - } + appendNl(sp.getString(R.string.notice_room_server_acl_updated_ip_literals_not_allowed)) } } + + if (!hasChanged) { + appendNl(sp.getString(R.string.notice_room_server_acl_updated_no_change)) + } } } From b8c89325bc0e660e5f414a379cbd3b9264bd3651 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 11:25:18 +0100 Subject: [PATCH 20/48] 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 8dff0b2c5dbd00f28882d8ab76cd2df218de8c52 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 15:33:44 +0100 Subject: [PATCH 21/48] Cleanup --- .../home/room/detail/timeline/format/NoticeEventFormatter.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index b5859ba1ba..c4cc2e87b0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -448,7 +448,6 @@ class NoticeEventFormatter @Inject constructor( .also { hasChanged = hasChanged || it.isNotEmpty() } .forEach { appendNl(sp.getString(R.string.notice_room_server_acl_updated_was_banned, it)) } - if (prevEventContent.allowIpLiterals != eventContent.allowIpLiterals) { hasChanged = true if (eventContent.allowIpLiterals) { From 8ae0501c22d0c52832d196bc411af57c6314080c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 16:17:27 +0100 Subject: [PATCH 22/48] Try to fix cropped image in timeline (#2126) --- CHANGES.md | 1 + .../app/features/media/ImageContentRenderer.kt | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9387ae161e..8c0ea5e421 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 + - Try to fix cropped image in timeline (#2126) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 4f1c52b240..187c2e85c3 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -21,6 +21,7 @@ import android.net.Uri import android.os.Parcelable import android.view.View import android.widget.ImageView +import androidx.core.view.updateLayoutParams import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.RoundedCorners @@ -96,15 +97,17 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: fun render(data: Data, mode: Mode, imageView: ImageView) { val size = processSize(data, mode) - imageView.layoutParams.width = size.width - imageView.layoutParams.height = size.height + imageView.updateLayoutParams { + width = size.width + height = size.height + } // a11y imageView.contentDescription = data.filename createGlideRequest(data, mode, imageView, size) .dontAnimate() .transform(RoundedCorners(dimensionConverter.dpToPx(8))) - .thumbnail(0.3f) + // .thumbnail(0.3f) .into(imageView) } @@ -117,6 +120,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: } } + /** + * Used by Attachment Viewer + */ fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) { val req = if (data.elementToDecrypt != null) { // Encrypted image From f2e8a9e0c74ef55f704e3f63ae7cb822bbf59e4e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Nov 2020 18:25:04 +0100 Subject: [PATCH 23/48] 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 24/48] 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 25/48] 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 26/48] 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 27/48] 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 28/48] 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 29/48] 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 30/48] 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 31/48] 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 32/48] 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 33/48] 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 34/48] 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 35/48] 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 36/48] 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 37/48] 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 38/48] 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 39/48] 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 40/48] 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 41/48] 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 42/48] 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 43/48] 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 44/48] 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 45/48] 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 46/48] 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 47/48] 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 48/48] 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" /> - +