mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-05-03 06:46:25 +00:00
Fixed text message description not being italic for some parts + improved description: added duration to voice message & replaced file name by image emoji for pictures
This commit is contained in:
parent
cbaf7673f5
commit
94f2c1cc98
9 changed files with 99 additions and 44 deletions
|
|
@ -778,7 +778,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
|
||||||
coreContext.contactsManager.findContactByAddress(address)
|
coreContext.contactsManager.findContactByAddress(address)
|
||||||
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(address)
|
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(address)
|
||||||
|
|
||||||
val originalMessage = LinphoneUtils.getTextDescribingMessage(message)
|
val originalMessage = LinphoneUtils.getPlainTextDescribingMessage(message)
|
||||||
val text = AppUtils.getString(R.string.notification_chat_message_reaction_received).format(
|
val text = AppUtils.getString(R.string.notification_chat_message_reaction_received).format(
|
||||||
reaction,
|
reaction,
|
||||||
originalMessage
|
originalMessage
|
||||||
|
|
@ -874,7 +874,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
|
||||||
coreContext.contactsManager.findContactByAddress(message.fromAddress)
|
coreContext.contactsManager.findContactByAddress(message.fromAddress)
|
||||||
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(message.fromAddress)
|
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(message.fromAddress)
|
||||||
|
|
||||||
val text = LinphoneUtils.getTextDescribingMessage(message)
|
val text = LinphoneUtils.getPlainTextDescribingMessage(message)
|
||||||
val notifiableMessage = NotifiableMessage(
|
val notifiableMessage = NotifiableMessage(
|
||||||
text,
|
text,
|
||||||
contact,
|
contact,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
*/
|
*/
|
||||||
package org.linphone.ui.main.chat.model
|
package org.linphone.ui.main.chat.model
|
||||||
|
|
||||||
|
import android.text.Spannable
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
@ -70,7 +71,9 @@ class ConversationModel @WorkerThread constructor(val chatRoom: ChatRoom) {
|
||||||
|
|
||||||
val isEphemeral = MutableLiveData<Boolean>()
|
val isEphemeral = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val lastMessageText = MutableLiveData<String>()
|
val lastMessageTextSender = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val lastMessageText = MutableLiveData<Spannable>()
|
||||||
|
|
||||||
val lastMessageIcon = MutableLiveData<Int>()
|
val lastMessageIcon = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
|
@ -254,7 +257,6 @@ class ConversationModel @WorkerThread constructor(val chatRoom: ChatRoom) {
|
||||||
private fun updateLastMessageStatus(message: ChatMessage) {
|
private fun updateLastMessageStatus(message: ChatMessage) {
|
||||||
val isOutgoing = message.isOutgoing
|
val isOutgoing = message.isOutgoing
|
||||||
|
|
||||||
val text = LinphoneUtils.getTextDescribingMessage(message)
|
|
||||||
if (isGroup && !isOutgoing) {
|
if (isGroup && !isOutgoing) {
|
||||||
val fromAddress = message.fromAddress
|
val fromAddress = message.fromAddress
|
||||||
val sender = coreContext.contactsManager.findContactByAddress(fromAddress)
|
val sender = coreContext.contactsManager.findContactByAddress(fromAddress)
|
||||||
|
|
@ -263,10 +265,11 @@ class ConversationModel @WorkerThread constructor(val chatRoom: ChatRoom) {
|
||||||
R.string.conversations_last_message_format,
|
R.string.conversations_last_message_format,
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
lastMessageText.postValue("$senderName $text")
|
lastMessageTextSender.postValue(senderName)
|
||||||
} else {
|
} else {
|
||||||
lastMessageText.postValue(text)
|
lastMessageTextSender.postValue("")
|
||||||
}
|
}
|
||||||
|
lastMessageText.postValue(LinphoneUtils.getFormattedTextDescribingMessage(message))
|
||||||
|
|
||||||
isLastMessageOutgoing.postValue(isOutgoing)
|
isLastMessageOutgoing.postValue(isOutgoing)
|
||||||
if (isOutgoing) {
|
if (isOutgoing) {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class EventLogModel @WorkerThread constructor(
|
||||||
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(from)
|
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(from)
|
||||||
replyTo = avatarModel.contactName ?: LinphoneUtils.getDisplayName(from)
|
replyTo = avatarModel.contactName ?: LinphoneUtils.getDisplayName(from)
|
||||||
|
|
||||||
LinphoneUtils.getTextDescribingMessage(replyMessage)
|
LinphoneUtils.getPlainTextDescribingMessage(replyMessage)
|
||||||
} else {
|
} else {
|
||||||
Log.e(
|
Log.e(
|
||||||
"$TAG Failed to find the reply message from ID [${chatMessage.replyMessageId}]"
|
"$TAG Failed to find the reply message from ID [${chatMessage.replyMessageId}]"
|
||||||
|
|
|
||||||
|
|
@ -510,12 +510,11 @@ class MessageModel @WorkerThread constructor(
|
||||||
filesList.postValue(filesPath)
|
filesList.postValue(filesPath)
|
||||||
|
|
||||||
if (!displayableContentFound) { // Temporary workaround to prevent empty bubbles
|
if (!displayableContentFound) { // Temporary workaround to prevent empty bubbles
|
||||||
val describe = LinphoneUtils.getTextDescribingMessage(chatMessage)
|
val describe = LinphoneUtils.getFormattedTextDescribingMessage(chatMessage)
|
||||||
Log.w(
|
Log.w(
|
||||||
"$TAG No displayable content found, generating text based description [$describe]"
|
"$TAG No displayable content found, generating text based description [$describe]"
|
||||||
)
|
)
|
||||||
val spannable = Spannable.Factory.getInstance().newSpannable(describe)
|
text.postValue(describe)
|
||||||
text.postValue(spannable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ package org.linphone.ui.main.chat.viewmodel
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.text.Spannable
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
|
@ -82,7 +83,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : GenericViewMo
|
||||||
|
|
||||||
val isReplyingTo = MutableLiveData<String>()
|
val isReplyingTo = MutableLiveData<String>()
|
||||||
|
|
||||||
val isReplyingToMessage = MutableLiveData<String>()
|
val isReplyingToMessage = MutableLiveData<Spannable>()
|
||||||
|
|
||||||
val isKeyboardOpen = MutableLiveData<Boolean>()
|
val isKeyboardOpen = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
|
@ -211,7 +212,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : GenericViewMo
|
||||||
Log.i("$TAG Pending reply to message [${message.messageId}]")
|
Log.i("$TAG Pending reply to message [${message.messageId}]")
|
||||||
chatMessageToReplyTo = message
|
chatMessageToReplyTo = message
|
||||||
isReplyingTo.postValue(model.avatarModel.value?.friend?.name)
|
isReplyingTo.postValue(model.avatarModel.value?.friend?.name)
|
||||||
isReplyingToMessage.postValue(LinphoneUtils.getTextDescribingMessage(message))
|
isReplyingToMessage.postValue(LinphoneUtils.getFormattedTextDescribingMessage(message))
|
||||||
isReplying.postValue(true)
|
isReplying.postValue(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,17 @@
|
||||||
*/
|
*/
|
||||||
package org.linphone.utils
|
package org.linphone.utils
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.StyleSpan
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.IntegerRes
|
import androidx.annotation.IntegerRes
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.contacts.getListOfSipAddresses
|
import org.linphone.contacts.getListOfSipAddresses
|
||||||
|
|
@ -391,9 +398,31 @@ class LinphoneUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun getTextDescribingMessage(message: ChatMessage): String {
|
fun getFormattedTextDescribingMessage(message: ChatMessage): Spannable {
|
||||||
|
val pair = getTextDescribingMessage(message)
|
||||||
|
val builder = SpannableStringBuilder(
|
||||||
|
"${pair.first} ${pair.second}".trim()
|
||||||
|
)
|
||||||
|
builder.setSpan(
|
||||||
|
StyleSpan(Typeface.ITALIC),
|
||||||
|
0,
|
||||||
|
pair.first.length,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
return builder.toSpannable()
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun getPlainTextDescribingMessage(message: ChatMessage): String {
|
||||||
|
val pair = getTextDescribingMessage(message)
|
||||||
|
return "${pair.first} ${pair.second}".trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun getTextDescribingMessage(message: ChatMessage): Pair<String, String> {
|
||||||
// If message contains text, then use that
|
// If message contains text, then use that
|
||||||
var text = message.contents.find { content -> content.isText }?.utf8Text ?: ""
|
var text = message.contents.find { content -> content.isText }?.utf8Text ?: ""
|
||||||
|
var contentDescription = ""
|
||||||
|
|
||||||
if (text.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
val firstContent = message.contents.firstOrNull()
|
val firstContent = message.contents.firstOrNull()
|
||||||
|
|
@ -402,26 +431,21 @@ class LinphoneUtils {
|
||||||
firstContent
|
firstContent
|
||||||
)
|
)
|
||||||
if (conferenceInfo != null) {
|
if (conferenceInfo != null) {
|
||||||
val subject = conferenceInfo.subject.orEmpty()
|
text = conferenceInfo.subject.orEmpty()
|
||||||
text = when (conferenceInfo.state) {
|
contentDescription = when (conferenceInfo.state) {
|
||||||
ConferenceInfo.State.Cancelled -> {
|
ConferenceInfo.State.Cancelled -> {
|
||||||
AppUtils.getFormattedString(
|
AppUtils.getString(
|
||||||
R.string.message_meeting_invitation_cancelled_content_description,
|
R.string.message_meeting_invitation_cancelled_content_description
|
||||||
subject
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ConferenceInfo.State.Updated -> {
|
ConferenceInfo.State.Updated -> {
|
||||||
AppUtils.getFormattedString(
|
AppUtils.getString(
|
||||||
R.string.message_meeting_invitation_updated_content_description,
|
R.string.message_meeting_invitation_updated_content_description
|
||||||
subject
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
AppUtils.getFormattedString(
|
AppUtils.getString(
|
||||||
R.string.message_meeting_invitation_content_description,
|
R.string.message_meeting_invitation_content_description
|
||||||
subject
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -432,18 +456,31 @@ class LinphoneUtils {
|
||||||
text = firstContent.name.orEmpty()
|
text = firstContent.name.orEmpty()
|
||||||
}
|
}
|
||||||
} else if (firstContent?.isVoiceRecording == true) {
|
} else if (firstContent?.isVoiceRecording == true) {
|
||||||
text = AppUtils.getString(R.string.message_voice_message_content_description)
|
val label = AppUtils.getString(
|
||||||
|
R.string.message_voice_message_content_description
|
||||||
|
)
|
||||||
|
val formattedDuration = SimpleDateFormat(
|
||||||
|
"mm:ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
).format(firstContent.fileDuration) // duration is in ms
|
||||||
|
contentDescription = "$label ($formattedDuration)"
|
||||||
} else {
|
} else {
|
||||||
for (content in message.contents) {
|
for (content in message.contents) {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
text += ", "
|
text += ", "
|
||||||
}
|
}
|
||||||
text += content.name
|
val contentType = "${content.type}/${content.subtype}"
|
||||||
|
text += when (FileUtils.getMimeType(contentType)) {
|
||||||
|
FileUtils.MimeType.Image -> "\uD83D\uDDBC\uFE0F"
|
||||||
|
// FileUtils.MimeType.Video -> "\uD83C\uDF9E\uFE0F"
|
||||||
|
// FileUtils.MimeType.Audio -> "\uD83C\uDFB5"
|
||||||
|
else -> content.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return text
|
return Pair(contentDescription, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,23 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toTopOf="@id/last_message_or_composing"/>
|
app:layout_constraintBottom_toTopOf="@id/last_message_or_composing"/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
style="@style/default_text_style"
|
||||||
|
android:id="@+id/last_message_sender"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="3sp"
|
||||||
|
android:gravity="center_vertical|start"
|
||||||
|
android:text="@{model.lastMessageTextSender, default=`John Doe:`}"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?attr/color_main2_400"
|
||||||
|
android:visibility="@{model.lastMessageTextSender.length() > 0 ? View.VISIBLE : View.GONE}"
|
||||||
|
textFont="@{model.isBeingDeleted || model.unreadMessageCount > 0 || model.isComposing ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/title"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/last_message_or_composing"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/separator"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
style="@style/default_text_style"
|
style="@style/default_text_style"
|
||||||
android:id="@+id/last_message_or_composing"
|
android:id="@+id/last_message_or_composing"
|
||||||
|
|
@ -92,10 +109,10 @@
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="?attr/color_main2_400"
|
android:textColor="?attr/color_main2_400"
|
||||||
textFont="@{model.isBeingDeleted || model.unreadMessageCount > 0 || model.isComposing ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}"
|
textFont="@{model.isBeingDeleted || model.unreadMessageCount > 0 || model.isComposing ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}"
|
||||||
app:layout_constraintStart_toStartOf="@id/title"
|
app:layout_constraintStart_toEndOf="@id/last_message_sender"
|
||||||
app:layout_constraintEnd_toStartOf="@id/right_border"
|
app:layout_constraintEnd_toStartOf="@id/right_border"
|
||||||
app:layout_constraintTop_toBottomOf="@id/title"
|
app:layout_constraintTop_toTopOf="@id/last_message_sender"
|
||||||
app:layout_constraintBottom_toTopOf="@id/separator" />
|
app:layout_constraintBottom_toBottomOf="@id/last_message_sender" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
style="@style/default_text_style_300"
|
style="@style/default_text_style_300"
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,11 @@
|
||||||
<string name="message_reaction_click_to_remove_label">Cliquez pour supprimer</string>
|
<string name="message_reaction_click_to_remove_label">Cliquez pour supprimer</string>
|
||||||
<string name="message_forwarded_label">Transféré</string>
|
<string name="message_forwarded_label">Transféré</string>
|
||||||
|
|
||||||
|
<string name="message_meeting_invitation_content_description">invitation à une réunion :</string>
|
||||||
|
<string name="message_meeting_invitation_updated_content_description">réunion mise à jour :</string>
|
||||||
|
<string name="message_meeting_invitation_cancelled_content_description">réunion annulée :</string>
|
||||||
|
<string name="message_voice_message_content_description">message vocal</string>
|
||||||
|
|
||||||
<!-- Scheduled conferences -->
|
<!-- Scheduled conferences -->
|
||||||
<string name="meetings_list_empty">Aucune réunion pour le moment…</string>
|
<string name="meetings_list_empty">Aucune réunion pour le moment…</string>
|
||||||
<string name="meetings_list_no_meeting_for_today">Aucune réunion aujourd\'hui</string>
|
<string name="meetings_list_no_meeting_for_today">Aucune réunion aujourd\'hui</string>
|
||||||
|
|
@ -714,12 +719,6 @@
|
||||||
<string name="assistant_forgotten_password"><u>Mot de passe oublié ?</u></string>
|
<string name="assistant_forgotten_password"><u>Mot de passe oublié ?</u></string>
|
||||||
<string name="call_zrtp_sas_validation_skip"><u>Passer</u></string>
|
<string name="call_zrtp_sas_validation_skip"><u>Passer</u></string>
|
||||||
|
|
||||||
<!-- Keep <i></i> in the following strings translations! -->
|
|
||||||
<string name="message_meeting_invitation_content_description"><i>invitation à une réunion : </i>%s</string>
|
|
||||||
<string name="message_meeting_invitation_updated_content_description"><i>réunion mise à jour : </i>%s</string>
|
|
||||||
<string name="message_meeting_invitation_cancelled_content_description"><i>réunion annulée : </i>%s</string>
|
|
||||||
<string name="message_voice_message_content_description"><i>message vocal</i></string>
|
|
||||||
|
|
||||||
<!-- Keep <b></b> in the following strings translations! -->
|
<!-- Keep <b></b> in the following strings translations! -->
|
||||||
<string name="conversation_message_meeting_updated_label"><b>La réunion a été mise à jour</b></string>
|
<string name="conversation_message_meeting_updated_label"><b>La réunion a été mise à jour</b></string>
|
||||||
<string name="conversation_message_meeting_cancelled_label"><b>La réunion a été annulée</b></string>
|
<string name="conversation_message_meeting_cancelled_label"><b>La réunion a été annulée</b></string>
|
||||||
|
|
|
||||||
|
|
@ -540,6 +540,11 @@
|
||||||
<string name="message_reaction_click_to_remove_label">Click to remove</string>
|
<string name="message_reaction_click_to_remove_label">Click to remove</string>
|
||||||
<string name="message_forwarded_label">Forwarded</string>
|
<string name="message_forwarded_label">Forwarded</string>
|
||||||
|
|
||||||
|
<string name="message_meeting_invitation_content_description">meeting invite:</string>
|
||||||
|
<string name="message_meeting_invitation_updated_content_description">meeting updated:</string>
|
||||||
|
<string name="message_meeting_invitation_cancelled_content_description">meeting cancelled:</string>
|
||||||
|
<string name="message_voice_message_content_description">voice message</string>
|
||||||
|
|
||||||
<!-- Scheduled conferences -->
|
<!-- Scheduled conferences -->
|
||||||
<string name="meetings_list_empty">No meeting for the moment…</string>
|
<string name="meetings_list_empty">No meeting for the moment…</string>
|
||||||
<string name="meetings_list_no_meeting_for_today">No meeting scheduled for today</string>
|
<string name="meetings_list_no_meeting_for_today">No meeting scheduled for today</string>
|
||||||
|
|
@ -752,12 +757,6 @@
|
||||||
<string name="assistant_forgotten_password"><u>Forgotten password?</u></string>
|
<string name="assistant_forgotten_password"><u>Forgotten password?</u></string>
|
||||||
<string name="call_zrtp_sas_validation_skip"><u>Skip</u></string>
|
<string name="call_zrtp_sas_validation_skip"><u>Skip</u></string>
|
||||||
|
|
||||||
<!-- Keep <i></i> in the following strings translations! -->
|
|
||||||
<string name="message_meeting_invitation_content_description"><i>meeting invite: </i>%s</string>
|
|
||||||
<string name="message_meeting_invitation_updated_content_description"><i>meeting updated: </i>%s</string>
|
|
||||||
<string name="message_meeting_invitation_cancelled_content_description"><i>meeting cancelled: </i>%s</string>
|
|
||||||
<string name="message_voice_message_content_description"><i>voice message</i></string>
|
|
||||||
|
|
||||||
<!-- Keep <b></b> in the following strings translations! -->
|
<!-- Keep <b></b> in the following strings translations! -->
|
||||||
<string name="conversation_message_meeting_updated_label"><b>Meeting has been updated</b></string>
|
<string name="conversation_message_meeting_updated_label"><b>Meeting has been updated</b></string>
|
||||||
<string name="conversation_message_meeting_cancelled_label"><b>Meeting has been cancelled!</b></string>
|
<string name="conversation_message_meeting_cancelled_label"><b>Meeting has been cancelled!</b></string>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue