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 3bbdf0f90..26e67f261 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -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 } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 00706c42e..85760a07f 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -45,15 +45,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 @@ -70,11 +73,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 @@ -209,7 +214,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 @@ -240,6 +244,9 @@ class ChatActivity : @Inject lateinit var dateUtils: DateUtils + @Inject + lateinit var colorUtil: ColorUtil + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -568,7 +575,7 @@ class ChatActivity : this.lifecycle.removeObserver(chatViewModel) } - @SuppressLint("NotifyDataSetChanged") + @SuppressLint("NotifyDataSetChanged", "SetTextI18n", "ResourceAsColor") @Suppress("LongMethod") private fun initObservers() { Log.d(TAG, "initObservers Called") @@ -684,9 +691,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 = @@ -1053,6 +1072,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( + 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(R.id.userAbsenceShortMessage).text = + String.format( + context.resources.getString(R.string.user_absence_for_one_day), + uiState.userAbsence.userId + ) + binding.outOfOfficeContainer.findViewById(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(R.id.userAbsenceShortMessage).text = + String.format( + context.resources.getString(R.string.user_absence), + uiState.userAbsence.userId + ) + + binding.outOfOfficeContainer.findViewById(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(R.id.absenceReplacement).text = + context.resources.getString(R.string.user_absence_replacement) + binding.outOfOfficeContainer.findViewById(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(R.id.replacement_user_name).text = + uiState.userAbsence.replacementUserDisplayName + } else { + binding.outOfOfficeContainer.findViewById(R.id.userAbsenceReplacement) + .visibility = View.GONE + } + binding.outOfOfficeContainer.findViewById(R.id.userAbsenceLongMessage).text = + uiState.userAbsence.message + binding.outOfOfficeContainer.findViewById(R.id.avatar_chip).setOnClickListener { + joinOneToOneConversation(uiState.userAbsence.replacementUserId!!) + } + } + } + } } private fun removeUnreadMessagesMarker() { @@ -3819,6 +3931,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 @@ -3871,7 +4001,10 @@ 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 OUT_OF_OFFICE_ALPHA = 76 } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt index 3b52c0a41..81a6ec6c8 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -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): Observable fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable fun editChatMessage(credentials: String, url: String, text: String): Observable + suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt index 080b0706f..eafab6484 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -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 { 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 { 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) + ) + } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 1798d7403..ed349b5fc 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -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 get() = _getVoiceRecordingLocked + private val _outOfOfficeViewState = MutableLiveData(OutOfOfficeUIState.None) + val outOfOfficeViewState: LiveData + get() = _outOfOfficeViewState + private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData> = MutableLiveData() val voiceMessagePlaybackSpeedPreferences: LiveData> 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() + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index b33c9ffbb..0efa202ce 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -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 diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceData.kt b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceData.kt new file mode 100644 index 000000000..77e71e8c7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceData.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * 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) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOCS.kt new file mode 100644 index 000000000..06a80c4ca --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * 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) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOverall.kt new file mode 100644 index 000000000..7f6fada53 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * 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) +} 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 5440e6bb1..755e8959b 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -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" + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt index 1d302d487..852c5a54e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt @@ -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) diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 23e4c9de8..a69daec52 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -127,6 +127,18 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 700af1ab2..75ff70baa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -839,4 +839,8 @@ How to translate with transifex: Failed to set conversation Read-only Status Reverted Your status was set automatically + + %1$s is out of office and might not respond + %1$s is out of office today + Replacement: