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
- 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)
- SIP URIs in chat messages are clickable to easily initiate a call
- New video call UI on foldable device like Galaxy Z Fold
- Setting to automatically record all calls
- 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() {
if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
findNavController().navigate(

View file

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

View file

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

View file

@ -24,12 +24,14 @@ import android.text.Spannable
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import androidx.lifecycle.MutableLiveData
import java.util.regex.Pattern
import org.linphone.R
import org.linphone.contact.GenericContactData
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.PatternClickableSpan
import org.linphone.utils.TimestampUtils
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
@ -182,7 +184,16 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
} else if (content.isText) {
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
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(
viewLifecycleOwner
) {

View file

@ -21,6 +21,7 @@ package org.linphone.activities.main.chat.views
import android.content.Context
import android.text.Layout
import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil
@ -40,6 +41,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
defStyleAttr: Int
) : 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) {
var wSpec = widthSpec
val widthMode = MeasureSpec.getMode(wSpec)

View file

@ -246,10 +246,8 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
val args = Bundle()
args.putString("URI", remoteAddress.asStringUriOnly())
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
args.putBoolean(
"SkipAutoCallStart",
true
) // If auto start call setting is enabled, ignore it
// If auto start call setting is enabled, ignore it
args.putBoolean("SkipAutoCallStart", true)
navigateToDialer(args)
} else {
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:text="@{data.text}"
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_width="wrap_content"
android:layout_height="wrap_content"