Merge branch 'master' into master

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2024-12-20 17:14:33 +01:00 committed by GitHub
commit c86769d0e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 430 additions and 9 deletions

View File

@ -167,7 +167,7 @@ ext {
espressoVersion = "3.6.1"
androidxTestVersion = "1.5.0"
media3_version = "1.4.1"
coroutines_version = "1.9.0"
coroutines_version = "1.10.0"
mockitoKotlinVersion = "5.4.0"
}
@ -327,7 +327,7 @@ dependencies {
androidTestImplementation "androidx.test:core:1.6.1"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.0"
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
androidTestImplementation 'org.mockito:mockito-android:5.14.2'
androidTestImplementation "androidx.work:work-testing:${workVersion}"

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

View File

@ -46,15 +46,18 @@ import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.AbsListView
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView
import androidx.core.content.FileProvider
import androidx.core.content.PermissionChecker
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toBitmap
import androidx.core.text.bold
import androidx.emoji2.text.EmojiCompat
@ -71,11 +74,13 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import coil.imageLoader
import coil.load
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.target.Target
import coil.transform.CircleCropTransformation
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.talk.BuildConfig
import com.nextcloud.talk.R
@ -210,7 +215,6 @@ import java.util.Date
import java.util.Locale
import java.util.concurrent.ExecutionException
import javax.inject.Inject
import kotlin.String
import kotlin.collections.set
import kotlin.math.roundToInt
@ -241,6 +245,9 @@ class ChatActivity :
@Inject
lateinit var dateUtils: DateUtils
@Inject
lateinit var colorUtil: ColorUtil
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@ -569,7 +576,7 @@ class ChatActivity :
this.lifecycle.removeObserver(chatViewModel)
}
@SuppressLint("NotifyDataSetChanged")
@SuppressLint("NotifyDataSetChanged", "SetTextI18n", "ResourceAsColor")
@Suppress("LongMethod")
private fun initObservers() {
Log.d(TAG, "initObservers Called")
@ -685,9 +692,21 @@ class ChatActivity :
loadAvatarForStatusBar()
setupSwipeToReply()
setActionBarTitle()
checkShowCallButtons()
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()
val urlForChatting =
@ -1054,6 +1073,99 @@ class ChatActivity :
chatViewModel.recordTouchObserver.observe(this) { 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() {
@ -3916,6 +4028,24 @@ class ChatActivity :
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 {
val TAG = ChatActivity::class.simpleName
private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
@ -3968,11 +4098,14 @@ class ChatActivity :
private const val FIVE_MINUTES_IN_SECONDS: Long = 300
private const val TEMPORARY_MESSAGE_ID_INT: Int = -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 NO_OFFLINE_MESSAGES_FOUND = "NO_OFFLINE_MESSAGES_FOUND"
const val VOICE_MESSAGE_CONTINUOUS_BEFORE = -5
const val VOICE_MESSAGE_CONTINUOUS_AFTER = 5
const val VOICE_MESSAGE_PLAY_ADD_THRESHOLD = 0.1
const val VOICE_MESSAGE_MARK_PLAYED_FACTOR = 20
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.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import io.reactivex.Observable
import retrofit2.Response
@ -63,4 +64,5 @@ interface ChatNetworkDataSource {
fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall>
fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall>
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
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel
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.generic.GenericOverall
import com.nextcloud.talk.models.json.reminder.Reminder
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observable
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> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
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> {
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.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
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.generic.GenericOverall
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.ui.PlaybackSpeed
import com.nextcloud.talk.utils.ConversationUtils
@ -47,6 +49,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
@ -109,6 +112,10 @@ class ChatViewModel @Inject constructor(
val getVoiceRecordingLocked: LiveData<Boolean>
get() = _getVoiceRecordingLocked
private val _outOfOfficeViewState = MutableLiveData<OutOfOfficeUIState>(OutOfOfficeUIState.None)
val outOfOfficeViewState: LiveData<OutOfOfficeUIState>
get() = _outOfOfficeViewState
private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData<Map<String, PlaybackSpeed>> = MutableLiveData()
val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>>
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 {
private val TAG = ChatViewModel::class.simpleName
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
fun provideChatNetworkDataSource(ncApi: NcApi): ChatNetworkDataSource {
return RetrofitChatNetwork(ncApi)
fun provideChatNetworkDataSource(ncApi: NcApi, ncApiCoroutines: NcApiCoroutines): ChatNetworkDataSource {
return RetrofitChatNetwork(ncApi, ncApiCoroutines)
}
@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
}
@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
fun getUrlForFederatedAvatar(
baseUrl: String,
@ -601,4 +607,8 @@ object ApiUtils {
fun getUrlForArchive(version: Int, baseUrl: String?, token: String?): String {
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))
}
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 {
val difference = (time2 - time1)
return abs(difference)

View File

@ -127,6 +127,18 @@
</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
android:id="@+id/messagesListView"
android:layout_width="match_parent"
@ -134,6 +146,7 @@
android:clipToPadding="false"
android:paddingBottom="20dp"
android:visibility="gone"
android:layout_below= "@id/out_of_office_container"
app:dateHeaderTextSize="13sp"
app:incomingBubblePaddingBottom="@dimen/message_bubble_corners_vertical_padding"
app:incomingBubblePaddingLeft="@dimen/message_bubble_corners_horizontal_padding"
@ -182,7 +195,9 @@
android:visibility="gone"
tools:visibility="visible"
app:background="@color/colorPrimary"
android:clipToPadding="false"
app:cornerRadius="@dimen/button_corner_radius"
app:icon="@drawable/ic_baseline_arrow_downward_24px" />
<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="status_reverted">Status Reverted</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>

View File

@ -24,7 +24,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:8.7.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
classpath "org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}"
classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.26'
classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.27'
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.7"
classpath "org.jlleitschuh.gradle:ktlint-gradle:12.1.2"
// NOTE: Do not place your application dependencies here; they belong

View File

@ -149,6 +149,7 @@
<trusted-key id="77A45740C23880C7F81B9D4D5C504E1210E49773">
<trusting group="com.mebigfatguy.fb-contrib" name="fb-contrib" version="7.6.5"/>
<trusting group="com.mebigfatguy.fb-contrib" name="fb-contrib" version="7.6.8"/>
<trusting group="com.mebigfatguy.fb-contrib" name="fb-contrib" version="7.6.9"/>
</trusted-key>
<trusted-key id="7B121B76A7ED6CE6E60AD51784E913A8E3A748C0" group="org.bouncycastle"/>
<trusted-key id="7E22D50A7EBD9D2CD269B2D4056ACA74D46000BF" group="io.netty"/>