Make SIP URI in chat messages clickable as well as http links
This commit is contained in:
parent
32941122ce
commit
46ef080d62
10 changed files with 126 additions and 6 deletions
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -354,4 +354,6 @@ class ChatMessageContentData(
|
||||||
|
|
||||||
interface OnContentClickedListener {
|
interface OnContentClickedListener {
|
||||||
fun onContentClicked(content: Content)
|
fun onContentClicked(content: Content)
|
||||||
|
|
||||||
|
fun onSipAddressClicked(sipUri: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
73
app/src/main/java/org/linphone/utils/PatternClickableSpan.kt
Normal file
73
app/src/main/java/org/linphone/utils/PatternClickableSpan.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue