Make SIP URI in chat messages clickable as well as http links

This commit is contained in:
Sylvain Berfini 2022-02-02 17:19:35 +01:00
parent 32941122ce
commit 46ef080d62
10 changed files with 126 additions and 6 deletions

View file

@ -20,6 +20,7 @@ Group changes to describe their impact on the project, as follows:
- Unread messages indicator in chat conversation that separates read & unread messages - Unread messages indicator in chat conversation that separates read & unread messages
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus) - Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus)
- Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING) - Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING)
- SIP URIs in chat messages are clickable to easily initiate a call
- New video call UI on foldable device like Galaxy Z Fold - New video call UI on foldable device like Galaxy Z Fold
- Setting to automatically record all calls - Setting to automatically record all calls
- When using a physical keyboard, use left control + enter keys to send message - When using a physical keyboard, use left control + enter keys to send message

View file

@ -322,6 +322,14 @@ internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() {
) )
} }
internal fun DetailChatRoomFragment.navigateToDialer(args: Bundle?) {
findMasterNavController().navigate(
R.id.action_global_dialerFragment,
args,
popupTo(R.id.dialerFragment, true)
)
}
internal fun ChatRoomCreationFragment.navigateToGroupInfo() { internal fun ChatRoomCreationFragment.navigateToGroupInfo() {
if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) { if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
findNavController().navigate( findNavController().navigate(

View file

@ -86,6 +86,10 @@ class ChatMessagesListAdapter(
MutableLiveData<Event<Content>>() MutableLiveData<Event<Content>>()
} }
val sipUriClickedEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy { val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
MutableLiveData<Event<ChatMessage>>() MutableLiveData<Event<ChatMessage>>()
} }
@ -94,6 +98,10 @@ class ChatMessagesListAdapter(
override fun onContentClicked(content: Content) { override fun onContentClicked(content: Content) {
openContentEvent.value = Event(content) openContentEvent.value = Event(content)
} }
override fun onSipAddressClicked(sipUri: String) {
sipUriClickedEvent.value = Event(sipUri)
}
} }
private var contextMenuDisabled: Boolean = false private var contextMenuDisabled: Boolean = false

View file

@ -354,4 +354,6 @@ class ChatMessageContentData(
interface OnContentClickedListener { interface OnContentClickedListener {
fun onContentClicked(content: Content) fun onContentClicked(content: Content)
fun onSipAddressClicked(sipUri: String)
} }

View file

@ -24,12 +24,14 @@ import android.text.Spannable
import android.text.util.Linkify import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat import androidx.core.text.util.LinkifyCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.util.regex.Pattern
import org.linphone.R import org.linphone.R
import org.linphone.contact.GenericContactData import org.linphone.contact.GenericContactData
import org.linphone.core.ChatMessage import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.PatternClickableSpan
import org.linphone.utils.TimestampUtils import org.linphone.utils.TimestampUtils
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) { class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
@ -182,7 +184,16 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
} else if (content.isText) { } else if (content.isText) {
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim()) val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS) LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
text.value = spannable text.value = PatternClickableSpan()
.add(
Pattern.compile("(sips?):([^@]+)(?:@([^ ]+))?"),
object : PatternClickableSpan.SpannableClickedListener {
override fun onSpanClicked(text: String) {
Log.i("[Chat Message Data] Clicked on SIP URI: $text")
contentListener?.onSipAddressClicked(text)
}
}
).build(spannable)
} }
} }

View file

@ -457,6 +457,19 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} }
} }
adapter.sipUriClickedEvent.observe(
viewLifecycleOwner
) {
it.consume { sipUri ->
val args = Bundle()
args.putString("URI", sipUri)
args.putBoolean("Transfer", false)
// If auto start call setting is enabled, ignore it
args.putBoolean("SkipAutoCallStart", true)
navigateToDialer(args)
}
}
adapter.scrollToChatMessageEvent.observe( adapter.scrollToChatMessageEvent.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {

View file

@ -21,6 +21,7 @@ package org.linphone.activities.main.chat.views
import android.content.Context import android.content.Context
import android.text.Layout import android.text.Layout
import android.text.method.LinkMovementMethod
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil import kotlin.math.ceil
@ -40,6 +41,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
defStyleAttr: Int defStyleAttr: Int
) : super(context, attrs, defStyleAttr) ) : super(context, attrs, defStyleAttr)
override fun setText(text: CharSequence?, type: BufferType?) {
super.setText(text, type)
// Required for PatternClickableSpan
movementMethod = LinkMovementMethod.getInstance()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) { override fun onMeasure(widthSpec: Int, heightSpec: Int) {
var wSpec = widthSpec var wSpec = widthSpec
val widthMode = MeasureSpec.getMode(wSpec) val widthMode = MeasureSpec.getMode(wSpec)

View file

@ -246,10 +246,8 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
val args = Bundle() val args = Bundle()
args.putString("URI", remoteAddress.asStringUriOnly()) args.putString("URI", remoteAddress.asStringUriOnly())
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
args.putBoolean( // If auto start call setting is enabled, ignore it
"SkipAutoCallStart", args.putBoolean("SkipAutoCallStart", true)
true
) // If auto start call setting is enabled, ignore it
navigateToDialer(args) navigateToDialer(args)
} else { } else {
val localAddress = callLogGroup.lastCallLog.localAddress val localAddress = callLogGroup.lastCallLog.localAddress

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2010-2022 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.utils
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
import java.util.regex.Pattern
class PatternClickableSpan {
var patterns: ArrayList<SpannablePatternItem> = ArrayList()
inner class SpannablePatternItem(
var pattern: Pattern,
var listener: SpannableClickedListener
)
interface SpannableClickedListener {
fun onSpanClicked(text: String)
}
inner class StyledClickableSpan(var item: SpannablePatternItem) : ClickableSpan() {
override fun onClick(widget: View) {
val tv = widget as TextView
val span = tv.text as Spanned
val start = span.getSpanStart(this)
val end = span.getSpanEnd(this)
val text = span.subSequence(start, end)
item.listener.onSpanClicked(text.toString())
}
}
fun add(
pattern: Pattern,
listener: SpannableClickedListener
): PatternClickableSpan {
patterns.add(SpannablePatternItem(pattern, listener))
return this
}
fun build(editable: CharSequence?): SpannableStringBuilder {
val ssb = SpannableStringBuilder(editable)
for (item in patterns) {
val matcher = item.pattern.matcher(ssb)
while (matcher.find()) {
val start = matcher.start()
val end = matcher.end()
val url = StyledClickableSpan(item)
ssb.setSpan(url, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
return ssb
}
}

View file

@ -187,7 +187,6 @@
android:onLongClick="@{contextMenuClickListener}" android:onLongClick="@{contextMenuClickListener}"
android:text="@{data.text}" android:text="@{data.text}"
android:visibility="@{data.text.length > 0 ? View.VISIBLE : View.GONE}" android:visibility="@{data.text.length > 0 ? View.VISIBLE : View.GONE}"
android:autoLink="web"
android:layout_gravity="@{data.chatMessage.outgoing ? Gravity.RIGHT : Gravity.LEFT}" android:layout_gravity="@{data.chatMessage.outgoing ? Gravity.RIGHT : Gravity.LEFT}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"