Profile: Start fetching profile info from a user

This commit is contained in:
ganfra 2020-01-13 18:44:01 +01:00
parent ae1a24e948
commit 162f0949fa
16 changed files with 404 additions and 87 deletions

View file

@ -26,7 +26,9 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import io.reactivex.Observable
import io.reactivex.Single
@ -56,7 +58,8 @@ class RxSession(private val session: Session) {
}
fun liveUser(userId: String): Observable<Optional<User>> {
return session.getUserLive(userId).asObservable().distinctUntilChanged()
return session.getUserLive(userId).asObservable()
.startWith(session.getUser(userId).toOptional())
}
fun liveUsers(): Observable<List<User>> {
@ -91,6 +94,11 @@ class RxSession(private val session: Session) {
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
}
fun getProfileInfo(userId: String): Single<JsonDict> = singleBuilder {
session.getProfile(userId, it)
}
}
fun Session.rx(): RxSession {

View file

@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
@ -51,6 +52,7 @@ interface Session :
SignOutService,
FilterService,
FileService,
ProfileService,
PushRuleService,
PushersService,
InitialSyncProgressService,

View file

@ -0,0 +1,39 @@
/*
* 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.
*
*/
package im.vector.matrix.android.api.session.profile
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
interface ProfileService {
companion object Constants {
const val DISPLAY_NAME_KEY = "displayname"
const val AVATAR_URL_KEY = "avatar_url"
}
fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
fun getProfile(userId: String, matrixCallback: MatrixCallback<JsonDict>): Cancelable
}

View file

@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
@ -76,6 +77,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val cryptoService: Lazy<DefaultCryptoService>,
private val fileService: Lazy<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
private val profileService: Lazy<ProfileService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore,
@ -97,7 +99,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get() {
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
ProfileService by profileService.get() {
private var isOpen = false

View file

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.session.filter.FilterModule
import im.vector.matrix.android.internal.session.group.GetGroupDataWorker
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule
import im.vector.matrix.android.internal.session.profile.ProfileModule
import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker
import im.vector.matrix.android.internal.session.pushers.PushersModule
import im.vector.matrix.android.internal.session.room.RoomModule
@ -64,6 +65,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
CryptoModule::class,
PushersModule::class,
AccountDataModule::class,
ProfileModule::class,
SessionAssistedInjectModule::class
]
)

View file

@ -0,0 +1,77 @@
/*
* 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.
*
*/
package im.vector.matrix.android.internal.session.profile
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import javax.inject.Inject
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
private val getProfileInfoTask: GetProfileInfoTask) : ProfileService {
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
val params = GetProfileInfoTask.Params(userId)
return getProfileInfoTask
.configureWith(params) {
this.callback = object : MatrixCallback<JsonDict> {
override fun onSuccess(data: JsonDict) {
val displayName = data[ProfileService.DISPLAY_NAME_KEY] as? String
matrixCallback.onSuccess(Optional.from(displayName))
}
override fun onFailure(failure: Throwable) {
matrixCallback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
val params = GetProfileInfoTask.Params(userId)
return getProfileInfoTask
.configureWith(params) {
this.callback = object : MatrixCallback<JsonDict> {
override fun onSuccess(data: JsonDict) {
val avatarUrl = data[ProfileService.AVATAR_URL_KEY] as? String
matrixCallback.onSuccess(Optional.from(avatarUrl))
}
override fun onFailure(failure: Throwable) {
matrixCallback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
override fun getProfile(userId: String, matrixCallback: MatrixCallback<JsonDict>): Cancelable {
val params = GetProfileInfoTask.Params(userId)
return getProfileInfoTask
.configureWith(params) {
this.callback = matrixCallback
}
.executeBy(taskExecutor)
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.
*
*/
package im.vector.matrix.android.internal.session.profile
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal abstract class GetProfileInfoTask : Task<GetProfileInfoTask.Params, JsonDict> {
data class Params(
val userId: String
)
}
internal class DefaultGetProfileInfoTask @Inject constructor(private val profileAPI: ProfileAPI) : GetProfileInfoTask() {
override suspend fun execute(params: Params): JsonDict {
return executeRequest {
apiCall = profileAPI.getProfile(params.userId)
}
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.
*
*/
package im.vector.matrix.android.internal.session.profile
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
interface ProfileAPI {
/**
* Get the combined profile information for this user. This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. This API may return keys which are not limited to displayname or avatar_url.
*
* @param userId the user id to fetch profile info
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}")
fun getProfile(@Path("userId") roomId: String): Call<JsonDict>
}

View file

@ -0,0 +1,45 @@
/*
* 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.
*
*/
package im.vector.matrix.android.internal.session.profile
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.internal.session.SessionScope
import retrofit2.Retrofit
@Module
internal abstract class ProfileModule {
@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesProfileAPI(retrofit: Retrofit): ProfileAPI {
return retrofit.create(ProfileAPI::class.java)
}
}
@Binds
abstract fun bindProfileService(userService: DefaultProfileService): ProfileService
@Binds
abstract fun bindGetProfileTask(getProfileInfoTask: DefaultGetProfileInfoTask): GetProfileInfoTask
}

View file

@ -25,7 +25,12 @@ import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.text.Spannable
import android.view.*
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.Window
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DrawableRes
@ -45,7 +50,13 @@ import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar
@ -55,7 +66,13 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -65,13 +82,31 @@ import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.*
import im.vector.riotx.core.utils.Debouncer
import im.vector.riotx.core.utils.KeyboardStateUtils
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_INCOMING_URI
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PICK_ATTACHMENT
import im.vector.riotx.core.utils.TextUtils
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.core.utils.getColorFromUserId
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.ContactAttachment
@ -84,7 +119,12 @@ import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedActi
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan
@ -94,7 +134,7 @@ import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.permalink.NavigateToRoomInterceptor
import im.vector.riotx.features.permalink.NavigationInterceptor
import im.vector.riotx.features.permalink.PermalinkHandler
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences
@ -247,9 +287,9 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
when (mode) {
is SendMode.REGULAR -> renderRegularMode(mode.text)
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
}
}
@ -276,9 +316,9 @@ class RoomDetailFragment @Inject constructor(
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) {
when (val sharedData = roomDetailArgs.sharedData) {
is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
null -> Timber.v("No share data to process")
null -> Timber.v("No share data to process")
}
}
}
@ -415,7 +455,7 @@ class RoomDetailFragment @Inject constructor(
avatarRenderer.render(
MatrixItem.UserItem(event.root.senderId
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar
)
@ -504,7 +544,7 @@ class RoomDetailFragment @Inject constructor(
is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
}
else -> false
else -> false
}
}
}
@ -519,9 +559,9 @@ class RoomDetailFragment @Inject constructor(
withState(roomDetailViewModel) {
val showJumpToUnreadBanner = when (it.unreadState) {
UnreadState.Unknown,
UnreadState.HasNoUnread -> false
UnreadState.HasNoUnread -> false
is UnreadState.ReadMarkerNotLoaded -> true
is UnreadState.HasUnread -> {
is UnreadState.HasUnread -> {
if (it.canShowJumpToReadMarker) {
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
@ -649,7 +689,7 @@ class RoomDetailFragment @Inject constructor(
navigator.openRoom(vectorBaseActivity, async())
vectorBaseActivity.finish()
}
is Fail -> {
is Fail -> {
vectorBaseActivity.hideWaitingView()
vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
}
@ -658,23 +698,23 @@ class RoomDetailFragment @Inject constructor(
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) {
is SendMessageResult.MessageSent -> {
is SendMessageResult.MessageSent -> {
updateComposerText("")
}
is SendMessageResult.SlashCommandHandled -> {
is SendMessageResult.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
updateComposerText("")
}
is SendMessageResult.SlashCommandError -> {
is SendMessageResult.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
}
is SendMessageResult.SlashCommandUnknown -> {
is SendMessageResult.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
}
is SendMessageResult.SlashCommandResultOk -> {
is SendMessageResult.SlashCommandResultOk -> {
updateComposerText("")
}
is SendMessageResult.SlashCommandResultError -> {
is SendMessageResult.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage)
}
is SendMessageResult.SlashCommandNotImplemented -> {
@ -712,7 +752,7 @@ class RoomDetailFragment @Inject constructor(
private fun displayRoomDetailActionResult(result: Async<RoomDetailAction>) {
when (result) {
is Fail -> {
is Fail -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.error))
@ -723,7 +763,7 @@ class RoomDetailFragment @Inject constructor(
when (val data = result.invoke()) {
is RoomDetailAction.ReportContent -> {
when {
data.spam -> {
data.spam -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content)
@ -745,7 +785,7 @@ class RoomDetailFragment @Inject constructor(
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
else -> {
else -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content)
@ -767,7 +807,7 @@ class RoomDetailFragment @Inject constructor(
override fun onUrlClicked(url: String): Boolean {
permalinkHandler
.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
.launch(requireActivity(), url, object : NavigationInterceptor {
override fun navToRoom(roomId: String?, eventId: String?): Boolean {
// Same room?
if (roomId == roomDetailArgs.roomId) {
@ -783,6 +823,11 @@ class RoomDetailFragment @Inject constructor(
// Not handled
return false
}
override fun navToMemberProfile(userId: String): Boolean {
navigator.openRoomMemberProfile(userId, roomDetailArgs.roomId, vectorBaseActivity)
return true
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -858,14 +903,14 @@ class RoomDetailFragment @Inject constructor(
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
when (requestCode) {
PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
val action = roomDetailViewModel.pendingAction
if (action != null) {
roomDetailViewModel.pendingAction = null
roomDetailViewModel.handle(action)
}
}
PERMISSION_REQUEST_CODE_INCOMING_URI -> {
PERMISSION_REQUEST_CODE_INCOMING_URI -> {
val pendingUri = roomDetailViewModel.pendingUri
if (pendingUri != null) {
roomDetailViewModel.pendingUri = null
@ -941,7 +986,7 @@ class RoomDetailFragment @Inject constructor(
override fun onRoomCreateLinkClicked(url: String) {
permalinkHandler
.launch(requireContext(), url, object : NavigateToRoomInterceptor {
.launch(requireContext(), url, object : NavigationInterceptor {
override fun navToRoom(roomId: String?, eventId: String?): Boolean {
requireActivity().finish()
return false
@ -963,23 +1008,23 @@ class RoomDetailFragment @Inject constructor(
private fun handleActions(action: EventSharedAction) {
when (action) {
is EventSharedAction.AddReaction -> {
is EventSharedAction.AddReaction -> {
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
}
is EventSharedAction.ViewReactions -> {
is EventSharedAction.ViewReactions -> {
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
is EventSharedAction.Copy -> {
is EventSharedAction.Copy -> {
// I need info about the current selected message :/
copyToClipboard(requireContext(), action.content, false)
val msg = requireContext().getString(R.string.copied_to_clipboard)
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
}
is EventSharedAction.Delete -> {
is EventSharedAction.Delete -> {
roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
}
is EventSharedAction.Share -> {
is EventSharedAction.Share -> {
// TODO current data communication is too limited
// Need to now the media type
// TODO bad, just POC
@ -1007,10 +1052,10 @@ class RoomDetailFragment @Inject constructor(
}
)
}
is EventSharedAction.ViewEditHistory -> {
is EventSharedAction.ViewEditHistory -> {
onEditedDecorationClicked(action.messageInformationData)
}
is EventSharedAction.ViewSource -> {
is EventSharedAction.ViewSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content
@ -1021,7 +1066,7 @@ class RoomDetailFragment @Inject constructor(
.setPositiveButton(R.string.ok, null)
.show()
}
is EventSharedAction.ViewDecryptedSource -> {
is EventSharedAction.ViewDecryptedSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content
@ -1032,31 +1077,31 @@ class RoomDetailFragment @Inject constructor(
.setPositiveButton(R.string.ok, null)
.show()
}
is EventSharedAction.QuickReact -> {
is EventSharedAction.QuickReact -> {
// eventId,ClickedOn,Add
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
}
is EventSharedAction.Edit -> {
is EventSharedAction.Edit -> {
roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString()))
}
is EventSharedAction.Quote -> {
is EventSharedAction.Quote -> {
roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString()))
}
is EventSharedAction.Reply -> {
is EventSharedAction.Reply -> {
roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString()))
}
is EventSharedAction.CopyPermalink -> {
is EventSharedAction.CopyPermalink -> {
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
}
is EventSharedAction.Resend -> {
is EventSharedAction.Resend -> {
roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
}
is EventSharedAction.Remove -> {
is EventSharedAction.Remove -> {
roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
}
is EventSharedAction.ReportContentSpam -> {
is EventSharedAction.ReportContentSpam -> {
roomDetailViewModel.handle(RoomDetailAction.ReportContent(
action.eventId, action.senderId, "This message is spam", spam = true))
}
@ -1064,19 +1109,19 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.ReportContent(
action.eventId, action.senderId, "This message is inappropriate", inappropriate = true))
}
is EventSharedAction.ReportContentCustom -> {
is EventSharedAction.ReportContentCustom -> {
promptReasonToReportContent(action)
}
is EventSharedAction.IgnoreUser -> {
is EventSharedAction.IgnoreUser -> {
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
}
is EventSharedAction.OnUrlClicked -> {
is EventSharedAction.OnUrlClicked -> {
onUrlClicked(action.url)
}
is EventSharedAction.OnUrlLongClicked -> {
is EventSharedAction.OnUrlLongClicked -> {
onUrlLongClicked(action.url)
}
else -> {
else -> {
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
}
}
@ -1092,7 +1137,7 @@ class RoomDetailFragment @Inject constructor(
val startToCompose = composerLayout.composerEditText.text.isNullOrBlank()
if (startToCompose
&& userId == session.myUserId) {
&& userId == session.myUserId) {
// Empty composer, current user: start an emote
composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(Command.EMOTE.length)
@ -1183,10 +1228,10 @@ class RoomDetailFragment @Inject constructor(
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
when (type) {
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
}

View file

@ -35,17 +35,17 @@ class PermalinkHandler @Inject constructor(private val session: Session,
fun launch(
context: Context,
deepLink: String?,
navigateToRoomInterceptor: NavigateToRoomInterceptor? = null,
navigationInterceptor: NavigationInterceptor? = null,
buildTask: Boolean = false
): Single<Boolean> {
val uri = deepLink?.let { Uri.parse(it) }
return launch(context, uri, navigateToRoomInterceptor, buildTask)
return launch(context, uri, navigationInterceptor, buildTask)
}
fun launch(
context: Context,
deepLink: Uri?,
navigateToRoomInterceptor: NavigateToRoomInterceptor? = null,
navigationInterceptor: NavigationInterceptor? = null,
buildTask: Boolean = false
): Single<Boolean> {
if (deepLink == null) {
@ -57,7 +57,7 @@ class PermalinkHandler @Inject constructor(private val session: Session,
.observeOn(AndroidSchedulers.mainThread())
.map {
val roomId = it.getOrNull()
if (navigateToRoomInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) {
if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) {
openRoom(context, roomId, permalinkData.eventId, buildTask)
}
true
@ -68,7 +68,9 @@ class PermalinkHandler @Inject constructor(private val session: Session,
Single.just(true)
}
is PermalinkData.UserLink -> {
navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask)
if (navigationInterceptor?.navToMemberProfile(permalinkData.userId) != true) {
navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask)
}
Single.just(true)
}
is PermalinkData.FallbackLink -> {
@ -98,10 +100,21 @@ class PermalinkHandler @Inject constructor(private val session: Session,
}
}
interface NavigateToRoomInterceptor {
interface NavigationInterceptor {
/**
* Return true if the navigation has been intercepted
*/
fun navToRoom(roomId: String?, eventId: String? = null): Boolean
fun navToRoom(roomId: String?, eventId: String? = null): Boolean {
return false
}
/**
* Return true if the navigation has been intercepted
*/
fun navToMemberProfile(userId: String): Boolean {
return false
}
}

View file

@ -40,9 +40,11 @@ class RoomMemberProfileController @Inject constructor(private val stringProvider
if (data == null) {
return
}
if (data.roomId == null) {
val roomMemberSummary = data.roomMemberSummary()
val profileInfo = data.profileInfo()
if (roomMemberSummary == null && profileInfo != null) {
buildUserActions()
} else {
} else if (roomMemberSummary != null) {
buildRoomMemberActions(data)
}
}

View file

@ -81,13 +81,15 @@ class RoomMemberProfileFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { state ->
val memberMatrixItem = state.memberAsMatrixItem() ?: return@withState
memberProfileIdView.text = memberMatrixItem.id
val bestName = memberMatrixItem.getBestName()
memberProfileNameView.text = bestName
matrixProfileToolbarTitleView.text = bestName
avatarRenderer.render(memberMatrixItem, memberProfileAvatarView)
avatarRenderer.render(memberMatrixItem, matrixProfileToolbarAvatarImageView)
val memberMatrixItem = state.memberAsMatrixItem()
if (memberMatrixItem != null) {
memberProfileIdView.text = memberMatrixItem.id
val bestName = memberMatrixItem.getBestName()
memberProfileNameView.text = bestName
matrixProfileToolbarTitleView.text = bestName
avatarRenderer.render(memberMatrixItem, memberProfileAvatarView)
avatarRenderer.render(memberMatrixItem, matrixProfileToolbarAvatarImageView)
}
val roomSummary = state.roomSummary()
val powerLevelsContent = state.powerLevelsContent()

View file

@ -64,17 +64,17 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
observeRoomSummary()
observeRoomMemberSummary()
observePowerLevel()
observeUserIfRequired()
fetchProfileInfoIfRequired()
}
private fun observeUserIfRequired() {
if (initialState.roomId != null) {
private fun fetchProfileInfoIfRequired() {
val roomMember = room?.getRoomMember(initialState.userId)
if (roomMember != null) {
return
}
session.rx().liveUser(initialState.userId)
.unwrap()
session.rx().getProfileInfo(initialState.userId)
.execute {
copy(user = it)
copy(profileInfo = it)
}
}

View file

@ -20,32 +20,33 @@ package im.vector.riotx.features.roommemberprofile
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
typealias ProfileInfo = JsonDict
data class RoomMemberProfileViewState(
val userId: String,
val roomId: String?,
val isMine: Boolean = false,
val roomSummary: Async<RoomSummary?> = Uninitialized,
val roomSummary: Async<RoomSummary> = Uninitialized,
val roomMemberSummary: Async<RoomMemberSummary> = Uninitialized,
val user: Async<User> = Uninitialized,
val profileInfo: Async<ProfileInfo> = Uninitialized,
val powerLevelsContent: Async<PowerLevelsContent> = Uninitialized
) : MvRxState {
constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId)
fun memberAsMatrixItem(): MatrixItem? {
return if (roomId == null) {
user.invoke()?.toMatrixItem()
} else {
roomMemberSummary.invoke()?.toMatrixItem()
return roomMemberSummary()?.toMatrixItem() ?: profileInfo()?.let {
MatrixItem.UserItem(userId, it[ProfileService.DISPLAY_NAME_KEY] as? String, it[ProfileService.AVATAR_URL_KEY] as? String)
}
}
}

View file

@ -9,6 +9,7 @@
android:padding="16dp"
android:orientation="vertical">
<ImageView
android:id="@+id/memberProfileAvatarView"
android:layout_width="128dp"