diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index 74a49ef53..351115c60 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBanOverall +import com.nextcloud.talk.models.json.profile.ProfileOverall import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import okhttp3.MultipartBody @@ -254,4 +255,7 @@ interface NcApiCoroutines { @GET suspend fun getNoteToSelfRoom(@Header("Authorization") authorization: String, @Url url: String): RoomOverall + + @GET + suspend fun getProfile(@Header("Authorization") authorization: String, @Url url: String): ProfileOverall } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index 83c967cf2..f9f5f0147 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -103,6 +103,11 @@ import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.launch import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.util.Calendar import java.util.Collections import java.util.Locale @@ -150,7 +155,7 @@ class ConversationInfoActivity : get() { if (!TextUtils.isEmpty(conversationToken)) { val data = Data.Builder() - data.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken) + data.putString(KEY_ROOM_TOKEN, conversationToken) data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!) return data.build() } @@ -192,7 +197,7 @@ class ConversationInfoActivity : conversationUser = currentUserProvider.currentUser.blockingGet() - conversationToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!! + conversationToken = intent.getStringExtra(KEY_ROOM_TOKEN)!! hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false) credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!! } @@ -335,6 +340,7 @@ class ConversationInfoActivity : } } + @SuppressLint("SetTextI18n") private fun initViewStateObserver() { viewModel.viewState.observe(this) { state -> when (state) { @@ -354,6 +360,10 @@ class ConversationInfoActivity : canGeneratePrettyURL ) } + + if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + viewModel.getProfileData(conversationUser, conversation!!.name) + } } is ConversationInfoViewModel.GetRoomErrorState -> { @@ -363,6 +373,37 @@ class ConversationInfoActivity : else -> {} } } + + viewModel.getProfileViewState.observe(this) { state -> + when (state) { + is ConversationInfoViewModel.GetProfileSuccessState -> { + val profile = state.profile + val pronouns = profile.pronouns ?: "" + binding.pronouns.text = pronouns + + val concat1 = if (profile.role != null && profile.company != null) " @ " else "" + val role = profile.role ?: "" + val company = profile.company ?: "" + val professionCompanyText = "$role$concat1$company" + binding.professionCompany.text = professionCompanyText + + val profileZoneOffset = ZoneOffset.ofTotalSeconds(0) + val secondsToAdd = profile.timezoneOffset?.toLong() ?: 0 + val localTime = ZonedDateTime.ofInstant(Instant.now().plusSeconds(secondsToAdd), profileZoneOffset) + val localTimeString = localTime.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)) + val concat2 = if (profile.address != null) " · " else "" + val address = profile.address ?: "" + val localTimeLocation = "$localTimeString$concat2$address" + binding.locationTime.text = resources.getString(R.string.local_time, localTimeLocation) + + binding.pronouns.visibility = VISIBLE + binding.professionCompany.visibility = if (professionCompanyText.isNotEmpty()) VISIBLE else GONE + binding.locationTime.visibility = VISIBLE + } + + else -> {} + } + } } private fun setupActionBar() { @@ -404,7 +445,7 @@ class ConversationInfoActivity : override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.edit) { val bundle = Bundle() - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken) + bundle.putString(KEY_ROOM_TOKEN, conversationToken) val intent = Intent(this, ConversationInfoEditActivity::class.java) intent.putExtras(bundle) @@ -445,7 +486,7 @@ class ConversationInfoActivity : intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName) - intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, conversationToken) + intent.putExtra(KEY_ROOM_TOKEN, conversationToken) intent.putExtra( SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR, ConversationUtils.isParticipantOwnerOrModerator(conversation!!) @@ -538,7 +579,7 @@ class ConversationInfoActivity : ) { binding.webinarInfoView.startTimeButtonSummary.text = ( dateUtils.getLocalDateTimeStringFromTimestamp( - conversation!!.lobbyTimer!! * DateConstants.SECOND_DIVIDER + conversation!!.lobbyTimer * DateConstants.SECOND_DIVIDER ) ) } else { @@ -1039,7 +1080,7 @@ class ConversationInfoActivity : binding.displayNameText.text = conversation!!.displayName - if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) { + if (conversation!!.description != null && conversation!!.description.isNotEmpty()) { binding.descriptionText.text = conversation!!.description binding.conversationDescription.visibility = VISIBLE } @@ -1185,7 +1226,7 @@ class ConversationInfoActivity : val stringValue: String = when ( DomainEnumNotificationLevelConverter() - .convertToInt(conversation!!.notificationLevel!!) + .convertToInt(conversation!!.notificationLevel) ) { NOTIFICATION_LEVEL_ALWAYS -> resources.getString(R.string.nc_notify_me_always) NOTIFICATION_LEVEL_MENTION -> resources.getString(R.string.nc_notify_me_mention) @@ -1234,7 +1275,7 @@ class ConversationInfoActivity : conversation!!.name ) ) { - conversation!!.name?.let { + conversation!!.name.let { binding.avatarImage.loadUserAvatar( conversationUser, it, diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt index 8ed98e29d..0874b17bd 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt @@ -28,6 +28,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.profile.Profile import com.nextcloud.talk.repositories.conversations.ConversationsRepository import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils.getUrlForRooms @@ -127,6 +128,12 @@ class ConversationInfoViewModel @Inject constructor( val createRoomViewState: LiveData get() = _createRoomViewState + object GetProfileErrorState : ViewState + class GetProfileSuccessState(val profile: Profile) : ViewState + private val _getProfileViewState = MutableLiveData() + val getProfileViewState: LiveData + get() = _getProfileViewState + fun getRoom(user: User, token: String) { _viewState.value = GetRoomStartState chatNetworkDataSource.getRoom(user, token) @@ -288,6 +295,18 @@ class ConversationInfoViewModel @Inject constructor( } } + fun getProfileData(user: User, userId: String) { + val url = ApiUtils.getUrlForProfile(user.baseUrl!!, userId) + viewModelScope.launch { + val profile = conversationsRepository.getProfile(user.getCredentials(), url) + if (profile != null) { + _getProfileViewState.value = GetProfileSuccessState(profile) + } else { + _getProfileViewState.value = GetProfileErrorState + } + } + } + @Suppress("Detekt.TooGenericExceptionCaught") fun allowGuests(token: String, allow: Boolean) { viewModelScope.launch { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/CoreProfileAction.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/CoreProfileAction.kt new file mode 100644 index 000000000..365f3244d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/CoreProfileAction.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class CoreProfileAction( + @JsonField(name = ["id"]) + var id: String? = null, + @JsonField(name = ["icon"]) + var icon: String? = null, + @JsonField(name = ["title"]) + var title: String? = null, + @JsonField(name = ["target"]) + var target: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/Profile.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/Profile.kt new file mode 100644 index 000000000..a23388b02 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/Profile.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Profile( + @JsonField(name = ["userId"]) var userId: String? = null, + @JsonField(name = ["address"]) var address: String? = null, + @JsonField(name = ["biography"]) var biography: Int? = null, + @JsonField(name = ["displayname"]) var displayName: Int? = null, + @JsonField(name = ["headline"]) var headline: String? = null, + // @JsonField(name = ["isUserAvatarVisible"]) var isUserAvatarVisible: Boolean = false, + @JsonField(name = ["organisation"]) var company: String? = null, + @JsonField(name = ["pronouns"]) var pronouns: String? = null, + @JsonField(name = ["role"]) var role: String? = null, + @JsonField(name = ["actions"]) var actions: List? = null, + @JsonField(name = ["timezone"]) var timezone: String? = null, + @JsonField(name = ["timezoneOffset"]) var timezoneOffset: Int? = null +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOCS.kt new file mode 100644 index 000000000..b26391ef1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ProfileOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta? = null, + @JsonField(name = ["data"]) + var data: Profile? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOverall.kt new file mode 100644 index 000000000..7ba466f6e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ProfileOverall( + @JsonField(name = ["ocs"]) + var ocs: ProfileOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt index c68b581d6..32a53e911 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt @@ -11,6 +11,7 @@ import com.nextcloud.talk.conversationinfo.CreateRoomRequest import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.profile.Profile import io.reactivex.Observable interface ConversationsRepository { @@ -46,4 +47,6 @@ interface ConversationsRepository { suspend fun clearChatHistory(apiVersion: Int, roomToken: String): GenericOverall suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall + + suspend fun getProfile(credentials: String, url: String): Profile? } diff --git a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt index 92886680e..d6e38753d 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt @@ -14,6 +14,7 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.profile.Profile import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew @@ -116,6 +117,10 @@ class ConversationsRepositoryImpl( return response } + override suspend fun getProfile(credentials: String, url: String): Profile? { + return coroutineApi.getProfile(credentials, url).ocs?.data + } + override suspend fun banActor( credentials: String, url: String, diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt index 2d30c2652..4cd1e6d68 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -625,4 +625,8 @@ object ApiUtils { fun getUrlForChatMessageContext(baseUrl: String, token: String, messageId: String): String { return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/chat/$token/$messageId/context" } + + fun getUrlForProfile(baseUrl: String, userId: String): String { + return "$baseUrl$OCS_API_VERSION/profile/$userId" + } } diff --git a/app/src/main/res/layout/activity_conversation_info.xml b/app/src/main/res/layout/activity_conversation_info.xml index 6024550dd..8bebc3938 100644 --- a/app/src/main/res/layout/activity_conversation_info.xml +++ b/app/src/main/res/layout/activity_conversation_info.xml @@ -86,8 +86,49 @@ android:layout_centerHorizontal="true" android:layout_marginTop="@dimen/margin_between_elements" android:textSize="@dimen/headline_text_size" + android:textStyle="bold" tools:text="Jane Doe" /> + + + + + + - + - + - + - + @@ -304,18 +345,18 @@ android:id="@+id/lock_conversation" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingStart="@dimen/standard_margin" - android:paddingEnd="@dimen/standard_margin" + android:background="?android:attr/selectableItemBackground" android:orientation="horizontal" - android:background="?android:attr/selectableItemBackground"> + android:paddingStart="@dimen/standard_margin" + android:paddingEnd="@dimen/standard_margin"> + android:orientation="horizontal" + android:paddingTop="@dimen/standard_half_margin" + android:paddingBottom="@dimen/standard_half_margin"> + android:paddingBottom="@dimen/standard_half_margin"> Archived %1$s Unarchived %1$s Conversation is archived + Local time: %1$s