Merge pull request #4521 from nextcloud/out_of_office

Out of office
This commit is contained in:
Sowjanya Kota 2024-12-20 15:23:04 +01:00 committed by GitHub
commit 5a7c45ef27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 426 additions and 6 deletions

View File

@ -13,6 +13,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.AddParticipantOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall
import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.participants.TalkBanOverall import com.nextcloud.talk.models.json.participants.TalkBanOverall
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import retrofit2.http.Body import retrofit2.http.Body
@ -197,4 +198,10 @@ interface NcApiCoroutines {
@Url url: String, @Url url: String,
@Field("seconds") seconds: Int @Field("seconds") seconds: Int
): GenericOverall ): GenericOverall
@GET
suspend fun getOutOfOfficeStatusForUser(
@Header("Authorization") authorization: String,
@Url url: String
): UserAbsenceOverall
} }

View File

@ -45,15 +45,18 @@ import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.AbsListView import android.widget.AbsListView
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.PermissionChecker import androidx.core.content.PermissionChecker
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.text.bold import androidx.core.text.bold
import androidx.emoji2.text.EmojiCompat import androidx.emoji2.text.EmojiCompat
@ -70,11 +73,13 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import autodagger.AutoInjector import autodagger.AutoInjector
import coil.imageLoader import coil.imageLoader
import coil.load
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.target.Target import coil.target.Target
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.color.ColorUtil
import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R import com.nextcloud.talk.R
@ -209,7 +214,6 @@ import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import javax.inject.Inject import javax.inject.Inject
import kotlin.String
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -240,6 +244,9 @@ class ChatActivity :
@Inject @Inject
lateinit var dateUtils: DateUtils lateinit var dateUtils: DateUtils
@Inject
lateinit var colorUtil: ColorUtil
@Inject @Inject
lateinit var viewModelFactory: ViewModelProvider.Factory lateinit var viewModelFactory: ViewModelProvider.Factory
@ -568,7 +575,7 @@ class ChatActivity :
this.lifecycle.removeObserver(chatViewModel) this.lifecycle.removeObserver(chatViewModel)
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged", "SetTextI18n", "ResourceAsColor")
@Suppress("LongMethod") @Suppress("LongMethod")
private fun initObservers() { private fun initObservers() {
Log.d(TAG, "initObservers Called") Log.d(TAG, "initObservers Called")
@ -684,9 +691,21 @@ class ChatActivity :
loadAvatarForStatusBar() loadAvatarForStatusBar()
setupSwipeToReply() setupSwipeToReply()
setActionBarTitle() setActionBarTitle()
checkShowCallButtons() checkShowCallButtons()
checkLobbyState() checkLobbyState()
if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
currentConversation?.status == "dnd"
) {
conversationUser?.let { user ->
val credentials = ApiUtils.getCredentials(user.username, user.token)
chatViewModel.outOfOfficeStatusOfUser(
credentials!!,
user.baseUrl!!,
currentConversation!!.name
)
}
}
updateRoomTimerHandler() updateRoomTimerHandler()
val urlForChatting = val urlForChatting =
@ -1053,6 +1072,99 @@ class ChatActivity :
chatViewModel.recordTouchObserver.observe(this) { y -> chatViewModel.recordTouchObserver.observe(this) { y ->
binding.voiceRecordingLock.y -= y binding.voiceRecordingLock.y -= y
} }
chatViewModel.outOfOfficeViewState.observe(this) { uiState ->
when (uiState) {
is ChatViewModel.OutOfOfficeUIState.Error -> {
Log.e(TAG, "Error fetching/ no user absence data", uiState.exception)
}
ChatViewModel.OutOfOfficeUIState.None -> {
}
is ChatViewModel.OutOfOfficeUIState.Success -> {
binding.outOfOfficeContainer.visibility = View.VISIBLE
val backgroundColor = colorUtil.getNullSafeColorWithFallbackRes(
conversationUser!!.capabilities!!.themingCapability!!.color,
R.color.colorPrimary
)
binding.outOfOfficeContainer.findViewById<View>(
R.id.verticalLine
).setBackgroundColor(backgroundColor)
val setAlpha = ColorUtils.setAlphaComponent(backgroundColor, OUT_OF_OFFICE_ALPHA)
binding.outOfOfficeContainer.setCardBackgroundColor(setAlpha)
val startDateTimestamp: Long = uiState.userAbsence.startDate.toLong()
val endDateTimestamp: Long = uiState.userAbsence.endDate.toLong()
val startDate = Date(startDateTimestamp * ONE_SECOND_IN_MILLIS)
val endDate = Date(endDateTimestamp * ONE_SECOND_IN_MILLIS)
if (dateUtils.isSameDate(startDate, endDate)) {
binding.outOfOfficeContainer.findViewById<TextView>(R.id.userAbsenceShortMessage).text =
String.format(
context.resources.getString(R.string.user_absence_for_one_day),
uiState.userAbsence.userId
)
binding.outOfOfficeContainer.findViewById<TextView>(R.id.userAbsencePeriod).visibility =
View.GONE
} else {
val dateFormatter = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
val startDateString = dateFormatter.format(startDate)
val endDateString = dateFormatter.format(endDate)
binding.outOfOfficeContainer.findViewById<TextView>(R.id.userAbsenceShortMessage).text =
String.format(
context.resources.getString(R.string.user_absence),
uiState.userAbsence.userId
)
binding.outOfOfficeContainer.findViewById<TextView>(R.id.userAbsencePeriod).text =
"$startDateString - $endDateString"
}
if (uiState.userAbsence.replacementUserDisplayName != null) {
var imageUri = Uri.parse(
ApiUtils.getUrlForAvatar(
conversationUser?.baseUrl,
uiState.userAbsence
.replacementUserId,
false
)
)
if (DisplayUtils.isDarkModeOn(context)) {
imageUri = Uri.parse(
ApiUtils.getUrlForAvatarDarkTheme(
conversationUser?.baseUrl,
uiState
.userAbsence
.replacementUserId,
false
)
)
}
binding.outOfOfficeContainer.findViewById<TextView>(R.id.absenceReplacement).text =
context.resources.getString(R.string.user_absence_replacement)
binding.outOfOfficeContainer.findViewById<ImageView>(R.id.replacement_user_avatar)
.load(imageUri) {
transformations(CircleCropTransformation())
placeholder(R.drawable.account_circle_96dp)
error(R.drawable.account_circle_96dp)
crossfade(true)
}
binding.outOfOfficeContainer.findViewById<TextView>(R.id.replacement_user_name).text =
uiState.userAbsence.replacementUserDisplayName
} else {
binding.outOfOfficeContainer.findViewById<LinearLayout>(R.id.userAbsenceReplacement)
.visibility = View.GONE
}
binding.outOfOfficeContainer.findViewById<TextView>(R.id.userAbsenceLongMessage).text =
uiState.userAbsence.message
binding.outOfOfficeContainer.findViewById<CardView>(R.id.avatar_chip).setOnClickListener {
joinOneToOneConversation(uiState.userAbsence.replacementUserId!!)
}
}
}
}
} }
private fun removeUnreadMessagesMarker() { private fun removeUnreadMessagesMarker() {
@ -3819,6 +3931,24 @@ class ChatActivity :
startActivity(shareIntent) startActivity(shareIntent)
} }
fun joinOneToOneConversation(userId: String) {
val apiVersion =
ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
apiVersion,
conversationUser?.baseUrl!!,
ROOM_TYPE_ONE_TO_ONE,
ACTOR_TYPE,
userId,
null
)
chatViewModel.createRoom(
credentials!!,
retrofitBucket.url!!,
retrofitBucket.queryMap!!
)
}
companion object { companion object {
val TAG = ChatActivity::class.simpleName val TAG = ChatActivity::class.simpleName
private const val CONTENT_TYPE_CALL_STARTED: Byte = 1 private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
@ -3871,7 +4001,10 @@ class ChatActivity :
private const val FIVE_MINUTES_IN_SECONDS: Long = 300 private const val FIVE_MINUTES_IN_SECONDS: Long = 300
private const val TEMPORARY_MESSAGE_ID_INT: Int = -3 private const val TEMPORARY_MESSAGE_ID_INT: Int = -3
private const val TEMPORARY_MESSAGE_ID_STRING: String = "-3" private const val TEMPORARY_MESSAGE_ID_STRING: String = "-3"
private const val ROOM_TYPE_ONE_TO_ONE = "1"
private const val ACTOR_TYPE = "users"
const val CONVERSATION_INTERNAL_ID = "CONVERSATION_INTERNAL_ID" const val CONVERSATION_INTERNAL_ID = "CONVERSATION_INTERNAL_ID"
const val NO_OFFLINE_MESSAGES_FOUND = "NO_OFFLINE_MESSAGES_FOUND" const val NO_OFFLINE_MESSAGES_FOUND = "NO_OFFLINE_MESSAGES_FOUND"
const val OUT_OF_OFFICE_ALPHA = 76
} }
} }

View File

@ -14,6 +14,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import io.reactivex.Observable import io.reactivex.Observable
import retrofit2.Response import retrofit2.Response
@ -63,4 +64,5 @@ interface ChatNetworkDataSource {
fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall> fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall>
fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall> fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall>
fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage> fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage>
suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall
} }

View File

@ -7,6 +7,7 @@
package com.nextcloud.talk.chat.data.network package com.nextcloud.talk.chat.data.network
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability import com.nextcloud.talk.models.json.capabilities.SpreedCapability
@ -15,11 +16,15 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observable import io.reactivex.Observable
import retrofit2.Response import retrofit2.Response
class RetrofitChatNetwork(private val ncApi: NcApi) : ChatNetworkDataSource { class RetrofitChatNetwork(
private val ncApi: NcApi,
private val ncApiCoroutines: NcApiCoroutines
) : ChatNetworkDataSource {
override fun getRoom(user: User, roomToken: String): Observable<ConversationModel> { override fun getRoom(user: User, roomToken: String): Observable<ConversationModel> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
@ -178,4 +183,15 @@ class RetrofitChatNetwork(private val ncApi: NcApi) : ChatNetworkDataSource {
override fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage> { override fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage> {
return ncApi.editChatMessage(credentials, url, text).map { it } return ncApi.editChatMessage(credentials, url, text).map { it }
} }
override suspend fun getOutOfOfficeStatusForUser(
credentials: String,
baseUrl: String,
userId: String
): UserAbsenceOverall {
return ncApiCoroutines.getOutOfOfficeStatusForUser(
credentials,
ApiUtils.getUrlForOutOfOffice(baseUrl, userId)
)
}
} }

View File

@ -16,6 +16,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
import com.nextcloud.talk.chat.data.io.MediaRecorderManager import com.nextcloud.talk.chat.data.io.MediaRecorderManager
@ -33,6 +34,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceData
import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.ui.PlaybackSpeed import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.ConversationUtils
@ -47,6 +49,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -109,6 +112,10 @@ class ChatViewModel @Inject constructor(
val getVoiceRecordingLocked: LiveData<Boolean> val getVoiceRecordingLocked: LiveData<Boolean>
get() = _getVoiceRecordingLocked get() = _getVoiceRecordingLocked
private val _outOfOfficeViewState = MutableLiveData<OutOfOfficeUIState>(OutOfOfficeUIState.None)
val outOfOfficeViewState: LiveData<OutOfOfficeUIState>
get() = _outOfOfficeViewState
private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData<Map<String, PlaybackSpeed>> = MutableLiveData() private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData<Map<String, PlaybackSpeed>> = MutableLiveData()
val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>> val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>>
get() = _voiceMessagePlaybackSpeedPreferences get() = _voiceMessagePlaybackSpeedPreferences
@ -764,8 +771,26 @@ class ChatViewModel @Inject constructor(
} }
} }
@Suppress("Detekt.TooGenericExceptionCaught")
fun outOfOfficeStatusOfUser(credentials: String, baseUrl: String, userId: String) {
viewModelScope.launch {
try {
val response = chatNetworkDataSource.getOutOfOfficeStatusForUser(credentials, baseUrl, userId)
_outOfOfficeViewState.value = OutOfOfficeUIState.Success(response.ocs?.data!!)
} catch (exception: Exception) {
_outOfOfficeViewState.value = OutOfOfficeUIState.Error(exception)
}
}
}
companion object { companion object {
private val TAG = ChatViewModel::class.simpleName private val TAG = ChatViewModel::class.simpleName
const val JOIN_ROOM_RETRY_COUNT: Long = 3 const val JOIN_ROOM_RETRY_COUNT: Long = 3
} }
sealed class OutOfOfficeUIState {
data object None : OutOfOfficeUIState()
data class Success(val userAbsence: UserAbsenceData) : OutOfOfficeUIState()
data class Error(val exception: Exception) : OutOfOfficeUIState()
}
} }

View File

@ -147,8 +147,8 @@ class RepositoryModule {
} }
@Provides @Provides
fun provideChatNetworkDataSource(ncApi: NcApi): ChatNetworkDataSource { fun provideChatNetworkDataSource(ncApi: NcApi, ncApiCoroutines: NcApiCoroutines): ChatNetworkDataSource {
return RetrofitChatNetwork(ncApi) return RetrofitChatNetwork(ncApi, ncApiCoroutines)
} }
@Provides @Provides

View File

@ -0,0 +1,38 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.userAbsence
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class UserAbsenceData(
@JsonField(name = ["id"])
var id: String,
@JsonField(name = ["userId"])
var userId: String,
@JsonField(name = ["startDate"])
var startDate: Int,
@JsonField(name = ["endDate"])
var endDate: Int,
@JsonField(name = ["shortMessage"])
var shortMessage: String,
@JsonField(name = ["message"])
var message: String,
@JsonField(name = ["replacementUserId"])
var replacementUserId: String?,
@JsonField(name = ["replacementUserDisplayName"])
var replacementUserDisplayName: String?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() :
this("", "", 0, 0, "", "", null, null)
}

View File

@ -0,0 +1,26 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.userAbsence
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 UserAbsenceOCS(
@JsonField(name = ["meta"])
var meta: GenericMeta?,
@JsonField(name = ["data"])
var data: UserAbsenceData?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null)
}

View File

@ -0,0 +1,23 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.userAbsence
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class UserAbsenceOverall(
@JsonField(name = ["ocs"])
var ocs: UserAbsenceOCS?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null)
}

View File

@ -372,6 +372,12 @@ object ApiUtils {
return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize
} }
@JvmStatic
fun getUrlForAvatarDarkTheme(baseUrl: String?, name: String?, requestBigSize: Boolean): String {
val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL
return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize + "/dark"
}
@JvmStatic @JvmStatic
fun getUrlForFederatedAvatar( fun getUrlForFederatedAvatar(
baseUrl: String, baseUrl: String,
@ -601,4 +607,8 @@ object ApiUtils {
fun getUrlForArchive(version: Int, baseUrl: String?, token: String?): String { fun getUrlForArchive(version: Int, baseUrl: String?, token: String?): String {
return "${getUrlForRoom(version, baseUrl, token)}/archive" return "${getUrlForRoom(version, baseUrl, token)}/archive"
} }
fun getUrlForOutOfOffice(baseUrl: String, userId: String): String {
return "$baseUrl$OCS_API_VERSION/apps/dav/api/v1/outOfOffice/$userId/now"
}
} }

View File

@ -51,6 +51,14 @@ class DateUtils(val context: Context) {
return formatTime.format(Date(timestampSeconds * DateConstants.SECOND_DIVIDER)) return formatTime.format(Date(timestampSeconds * DateConstants.SECOND_DIVIDER))
} }
fun isSameDate(date1: Date, date2: Date): Boolean {
val startDateCalendar = Calendar.getInstance().apply { time = date1 }
val endDateCalendar = Calendar.getInstance().apply { time = date2 }
val isSameDay = startDateCalendar.get(Calendar.YEAR) == endDateCalendar.get(Calendar.YEAR) &&
startDateCalendar.get(Calendar.DAY_OF_YEAR) == endDateCalendar.get(Calendar.DAY_OF_YEAR)
return isSameDay
}
fun getTimeDifferenceInSeconds(time2: Long, time1: Long): Long { fun getTimeDifferenceInSeconds(time2: Long, time1: Long): Long {
val difference = (time2 - time1) val difference = (time2 - time1)
return abs(difference) return abs(difference)

View File

@ -127,6 +127,18 @@
</LinearLayout> </LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/out_of_office_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
android:layout_margin="8dp"
app:cardCornerRadius="12dp">
<include layout="@layout/out_of_office_view" />
</com.google.android.material.card.MaterialCardView>
<com.stfalcon.chatkit.messages.MessagesList <com.stfalcon.chatkit.messages.MessagesList
android:id="@+id/messagesListView" android:id="@+id/messagesListView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -134,6 +146,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="20dp" android:paddingBottom="20dp"
android:visibility="gone" android:visibility="gone"
android:layout_below= "@id/out_of_office_container"
app:dateHeaderTextSize="13sp" app:dateHeaderTextSize="13sp"
app:incomingBubblePaddingBottom="@dimen/message_bubble_corners_vertical_padding" app:incomingBubblePaddingBottom="@dimen/message_bubble_corners_vertical_padding"
app:incomingBubblePaddingLeft="@dimen/message_bubble_corners_horizontal_padding" app:incomingBubblePaddingLeft="@dimen/message_bubble_corners_horizontal_padding"
@ -182,7 +195,9 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" tools:visibility="visible"
app:background="@color/colorPrimary" app:background="@color/colorPrimary"
android:clipToPadding="false"
app:cornerRadius="@dimen/button_corner_radius" app:cornerRadius="@dimen/button_corner_radius"
app:icon="@drawable/ic_baseline_arrow_downward_24px" /> app:icon="@drawable/ic_baseline_arrow_downward_24px" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
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">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/out_of_office_view"
app:layout_constraintHeight_min="0dp"
app:layout_constraintHeight_max="150dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<View
android:id="@+id/verticalLine"
android:layout_width="6dp"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/userAbsenceShortMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Jane is out of office"/>
<TextView
android:id="@+id/userAbsencePeriod"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop ="8dp"
android:textSize="14sp"
tools:text="Dec 5, 2024 - Dec 15, 2024"/>
<LinearLayout
android:id="@+id/userAbsenceReplacement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop ="8dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/absenceReplacement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
tools:text="Replacement: "/>
<androidx.cardview.widget.CardView
android:id="@+id/avatar_chip"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_marginStart="8dp"
app:cardCornerRadius="16dp"
android:layout_gravity="center_vertical"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding = "4dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/replacement_user_avatar"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="4dp"
android:scaleType="centerCrop"
android:contentDescription="@null" />
<TextView
android:id="@+id/replacement_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:gravity="center_vertical"
tools:text="Bob" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<TextView
android:id="@+id/userAbsenceLongMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop ="8dp"
android:textSize="14sp"
tools:text="Hi, I am out of office this week. Please contact ....., ..........write very very very very very very very very very very very very very very very long message..................................................................................................................................if you have any issues."/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -839,4 +839,8 @@ How to translate with transifex:
<string name="conversation_read_only_failed">Failed to set conversation Read-only</string> <string name="conversation_read_only_failed">Failed to set conversation Read-only</string>
<string name="status_reverted">Status Reverted</string> <string name="status_reverted">Status Reverted</string>
<string name="automatic_status_set">Your status was set automatically</string> <string name="automatic_status_set">Your status was set automatically</string>
<string name="user_absence">%1$s is out of office and might not respond</string>
<string name="user_absence_for_one_day">%1$s is out of office today</string>
<string name="user_absence_replacement">Replacement: </string>
</resources> </resources>