Merge pull request #1284 from nextcloud/feature/1136/share-location

Feature/1136/share location
This commit is contained in:
Andy Scherzinger 2021-06-11 17:00:48 +02:00 committed by GitHub
commit 9c18e1c085
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2648 additions and 207 deletions

View File

@ -113,7 +113,9 @@ android {
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/rxjava.properties'
}
@ -267,7 +269,10 @@ dependencies {
implementation 'com.github.Kennyc1012:BottomSheet:2.4.1'
implementation 'com.github.nextcloud:PopupBubble:master-SNAPSHOT'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'eu.medsea.mimeutil:mime-util:2.1.3'
implementation('eu.medsea.mimeutil:mime-util:2.1.3', {
exclude group: 'org.slf4j'
})
implementation "com.afollestad.material-dialogs:core:${materialDialogsVersion}"
implementation "com.afollestad.material-dialogs:datetime:${materialDialogsVersion}"
@ -286,6 +291,12 @@ dependencies {
implementation 'com.github.tobiaskaminsky:ImagePicker:extraFile-SNAPSHOT'
implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0'
implementation 'org.osmdroid:osmdroid-android:6.1.10'
implementation ('fr.dudie:nominatim-api:3.4', {
//noinspection DuplicatePlatformClasses
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
})
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:3.11.0'
testImplementation "org.powermock:powermock-core:${powermockVersion}"

View File

@ -70,6 +70,9 @@
<!-- This permission is deprecated in Android P -->
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:name=".application.NextcloudTalkApplication"
android:allowBackup="true"

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-Type' content='text/html; charset=UTF-8' />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<style>
html, body, #map {
height: 100%;
}
body {
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<div id="map" ></div>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script>
var queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
var locationLat = urlParams.get('locationLat')
var locationLon = urlParams.get('locationLon')
var locationGeoLink = urlParams.get('locationGeoLink')
var mapProviderUrl = urlParams.get('mapProviderUrl')
var mapProviderAttribution = urlParams.get('mapProviderAttribution')
var map = L.map('map', {
zoomControl: false,
scrollWheelZoom: false
}).setView([locationLat, locationLon], 13);
map.dragging.disable();
L.tileLayer(mapProviderUrl, {
attribution: '&copy; ' + mapProviderAttribution
}).addTo(map);
L.marker([locationLat, locationLon]).addTo(map);
</script>
</body>
</html>

View File

@ -0,0 +1,58 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
*
* 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 com.nextcloud.talk.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import com.nextcloud.talk.R
import fr.dudie.nominatim.model.Address
class GeocodingAdapter(context: Context, val dataSource: List<Address>) : BaseAdapter() {
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
override fun getCount(): Int {
return dataSource.size
}
override fun getItem(position: Int): Any {
return dataSource[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val rowView = inflater.inflate(R.layout.geocoding_item, parent, false)
val nameView = rowView.findViewById(R.id.name) as TextView
val address = getItem(position) as Address
nameView.text = address.displayName
return rowView
}
}

View File

@ -0,0 +1,269 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* @author Andy Scherzinger
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* 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 com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.text.TextUtils
import android.util.Log
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.ViewCompat
import autodagger.AutoInjector
import coil.load
import com.amulyakhare.textdrawable.TextDrawable
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import java.net.URLEncoder
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class IncomingLocationMessageViewHolder(incomingView: View) : MessageHolders
.IncomingTextMessageViewHolder<ChatMessage>(incomingView) {
private val binding: ItemCustomIncomingLocationMessageBinding =
ItemCustomIncomingLocationMessageBinding.bind(itemView)
var locationLon: String? = ""
var locationLat: String? = ""
var locationName: String? = ""
var locationGeoLink: String? = ""
@JvmField
@Inject
var context: Context? = null
@JvmField
@Inject
var appPreferences: AppPreferences? = null
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
super.onBind(message)
sharedApplication!!.componentApplication.inject(this)
setAvatarAndAuthorOnMessageItem(message)
colorizeMessageBubble(message)
itemView.isSelected = false
binding.messageTime.setTextColor(context?.resources!!.getColor(R.color.warm_grey_four))
val textSize = context?.resources!!.getDimension(R.dimen.chat_text_size)
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageText.text = message.text
binding.messageText.isEnabled = false
// parent message handling
setParentMessageDataOnMessageItem(message)
// geo-location
setLocationDataOnMessageItem(message)
}
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
val author: String = message.actorDisplayName
if (!TextUtils.isEmpty(author)) {
binding.messageAuthor.text = author
} else {
binding.messageAuthor.setText(R.string.nc_nick_guest)
}
if (!message.isGrouped && !message.isOneToOneConversation) {
binding.messageUserAvatar.visibility = View.VISIBLE
if (message.actorType == "guests") {
// do nothing, avatar is set
} else if (message.actorType == "bots" && message.actorId == "changelog") {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = AppCompatResources.getDrawable(context!!, R.drawable.ic_launcher_background)
layers[1] = AppCompatResources.getDrawable(context!!, R.drawable.ic_launcher_foreground)
val layerDrawable = LayerDrawable(layers)
binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
} else if (message.actorType == "bots") {
val drawable = TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(
">",
context!!.resources.getColor(R.color.black)
)
binding.messageUserAvatar.visibility = View.VISIBLE
binding.messageUserAvatar.setImageDrawable(drawable)
}
} else {
if (message.isOneToOneConversation) {
binding.messageUserAvatar.visibility = View.GONE
} else {
binding.messageUserAvatar.visibility = View.INVISIBLE
}
binding.messageAuthor.visibility = View.GONE
}
}
private fun colorizeMessageBubble(message: ChatMessage) {
val resources = itemView.resources
var bubbleResource = R.drawable.shape_incoming_message
if (message.isGrouped) {
bubbleResource = R.drawable.shape_grouped_incoming_message
}
val bgBubbleColor = if (message.isDeleted) {
resources.getColor(R.color.bg_message_list_incoming_bubble_deleted)
} else {
resources.getColor(R.color.bg_message_list_incoming_bubble)
}
val bubbleDrawable = DisplayUtils.getMessageSelector(
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor, bubbleResource
)
ViewCompat.setBackground(bubble, bubbleDrawable)
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = parentChatMessage.text
binding.messageQuote.quotedMessageAuthor
.setTextColor(context!!.resources.getColor(R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser.userId) == true) {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
} else {
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}
}
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
private fun setLocationDataOnMessageItem(message: ChatMessage) {
if (message.messageParameters != null && message.messageParameters.size > 0) {
for (key in message.messageParameters.keys) {
val individualHashMap: Map<String, String> = message.messageParameters[key]!!
if (individualHashMap["type"] == "geo-location") {
locationLon = individualHashMap["longitude"]
locationLat = individualHashMap["latitude"]
locationName = individualHashMap["name"]
locationGeoLink = individualHashMap["id"]
}
}
}
binding.webview.settings?.javaScriptEnabled = true
binding.webview.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return if (url != null && (url.startsWith("http://") || url.startsWith("https://"))
) {
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
true
} else {
false
}
}
}
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
urlStringBuffer.append(
"?mapProviderUrl=" + URLEncoder.encode(context!!.getString(R.string.osm_tile_server_url))
)
urlStringBuffer.append(
"&mapProviderAttribution=" + URLEncoder.encode(context!!.getString(R.string.osm_tile_server_attributation))
)
urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat))
urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon))
urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName))
urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink))
binding.webview.loadUrl(urlStringBuffer.toString())
binding.webview.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_UP -> openGeoLink()
}
return v?.onTouchEvent(event) ?: true
}
})
}
private fun openGeoLink() {
if (!locationGeoLink.isNullOrEmpty()) {
val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!)
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(geoLinkWithMarker))
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context!!.startActivity(browserIntent)
} else {
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
Log.e(TAG, "locationGeoLink was null or empty")
}
}
private fun addMarkerToGeoLink(locationGeoLink: String): String {
return locationGeoLink.replace("geo:", "geo:0,0?q=")
}
companion object {
private const val TAG = "LocInMessageView"
}
}

View File

@ -0,0 +1,45 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
*
* 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 com.nextcloud.talk.adapters.messages;
import android.view.View;
import android.widget.ProgressBar;
import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
import androidx.emoji.widget.EmojiTextView;
public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder {
private final ItemCustomIncomingPreviewMessageBinding binding;
public IncomingPreviewMessageViewHolder(View itemView) {
super(itemView);
binding = ItemCustomIncomingPreviewMessageBinding.bind(itemView);
}
public EmojiTextView getMessageText() {
return binding.messageText;
}
public ProgressBar getProgressBar() {
return binding.progressBar;
}
}

View File

@ -2,6 +2,8 @@
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
@ -30,20 +32,14 @@ import android.text.SpannableString
import android.text.TextUtils
import android.util.TypedValue
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.emoji.widget.EmojiTextView
import autodagger.AutoInjector
import butterknife.BindView
import butterknife.ButterKnife
import coil.load
import com.amulyakhare.textdrawable.TextDrawable
import com.facebook.drawee.view.SimpleDraweeView
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils
@ -57,41 +53,7 @@ import javax.inject.Inject
class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
.IncomingTextMessageViewHolder<ChatMessage>(itemView) {
@JvmField
@BindView(R.id.messageAuthor)
var messageAuthor: EmojiTextView? = null
@JvmField
@BindView(R.id.messageText)
var messageText: EmojiTextView? = null
@JvmField
@BindView(R.id.messageUserAvatar)
var messageUserAvatarView: SimpleDraweeView? = null
@JvmField
@BindView(R.id.messageTime)
var messageTimeView: TextView? = null
@JvmField
@BindView(R.id.quotedChatMessageView)
var quotedChatMessageView: RelativeLayout? = null
@JvmField
@BindView(R.id.quotedMessageAuthor)
var quotedUserName: EmojiTextView? = null
@JvmField
@BindView(R.id.quotedMessageImage)
var quotedMessagePreview: ImageView? = null
@JvmField
@BindView(R.id.quotedMessage)
var quotedMessage: EmojiTextView? = null
@JvmField
@BindView(R.id.quoteColoredView)
var quoteColoredView: View? = null
private val binding: ItemCustomIncomingTextMessageBinding = ItemCustomIncomingTextMessageBinding.bind(itemView)
@JvmField
@Inject
@ -101,22 +63,18 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
@Inject
var appPreferences: AppPreferences? = null
init {
ButterKnife.bind(this, itemView)
}
override fun onBind(message: ChatMessage) {
super.onBind(message)
sharedApplication!!.componentApplication.inject(this)
val author: String = message.actorDisplayName
if (!TextUtils.isEmpty(author)) {
messageAuthor!!.text = author
binding.messageAuthor.text = author
} else {
messageAuthor!!.setText(R.string.nc_nick_guest)
binding.messageAuthor.setText(R.string.nc_nick_guest)
}
if (!message.isGrouped && !message.isOneToOneConversation) {
messageUserAvatarView!!.visibility = View.VISIBLE
binding.messageUserAvatar.visibility = View.VISIBLE
if (message.actorType == "guests") {
// do nothing, avatar is set
} else if (message.actorType == "bots" && message.actorId == "changelog") {
@ -124,7 +82,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
layers[0] = context?.getDrawable(R.drawable.ic_launcher_background)
layers[1] = context?.getDrawable(R.drawable.ic_launcher_foreground)
val layerDrawable = LayerDrawable(layers)
messageUserAvatarView?.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
} else if (message.actorType == "bots") {
val drawable = TextDrawable.builder()
.beginConfig()
@ -134,16 +92,16 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
">",
context!!.resources.getColor(R.color.black)
)
messageUserAvatarView!!.visibility = View.VISIBLE
messageUserAvatarView?.setImageDrawable(drawable)
binding.messageUserAvatar.visibility = View.VISIBLE
binding.messageUserAvatar.setImageDrawable(drawable)
}
} else {
if (message.isOneToOneConversation) {
messageUserAvatarView!!.visibility = View.GONE
binding.messageUserAvatar.visibility = View.GONE
} else {
messageUserAvatarView!!.visibility = View.INVISIBLE
binding.messageUserAvatar.visibility = View.INVISIBLE
}
messageAuthor!!.visibility = View.GONE
binding.messageAuthor.visibility = View.GONE
}
val resources = itemView.resources
@ -170,7 +128,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
val messageParameters = message.messageParameters
itemView.isSelected = false
messageTimeView!!.setTextColor(context?.resources!!.getColor(R.color.warm_grey_four))
binding.messageTime.setTextColor(context?.resources!!.getColor(R.color.warm_grey_four))
var messageString: Spannable = SpannableString(message.text)
@ -187,7 +145,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
) {
if (individualHashMap["id"] == message.activeUser!!.userId) {
messageString = DisplayUtils.searchAndReplaceWithMentionSpan(
messageText!!.context,
binding.messageText.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
@ -197,7 +155,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
)
} else {
messageString = DisplayUtils.searchAndReplaceWithMentionSpan(
messageText!!.context,
binding.messageText.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
@ -217,43 +175,43 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
textSize = (textSize * 2.5).toFloat()
itemView.isSelected = true
messageAuthor!!.visibility = View.GONE
binding.messageAuthor.visibility = View.GONE
}
messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
messageText!!.text = messageString
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageText.text = messageString
// parent message handling
if (!message.isDeleted && message.parentMessage != null) {
var parentChatMessage = message.parentMessage
val parentChatMessage = message.parentMessage
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
quotedMessagePreview?.visibility = View.VISIBLE
quotedMessagePreview?.load(it) {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
)
}
} ?: run {
quotedMessagePreview?.visibility = View.GONE
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
quotedUserName?.text = parentChatMessage.actorDisplayName
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
quotedMessage?.text = parentChatMessage.text
binding.messageQuote.quotedMessage.text = parentChatMessage.text
quotedUserName?.setTextColor(context!!.resources.getColor(R.color.textColorMaxContrast))
binding.messageQuote.quotedMessageAuthor
.setTextColor(context!!.resources.getColor(R.color.textColorMaxContrast))
if (parentChatMessage.actorId?.equals(message.activeUser.userId) == true) {
quoteColoredView?.setBackgroundResource(R.color.colorPrimary)
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
} else {
quoteColoredView?.setBackgroundResource(R.color.textColorMaxContrast)
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
}
quotedChatMessageView?.visibility = View.VISIBLE
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
quotedChatMessageView?.visibility = View.GONE
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)

View File

@ -2,6 +2,8 @@
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
@ -27,19 +29,14 @@ import android.text.Spannable
import android.text.SpannableString
import android.util.TypedValue
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.emoji.widget.EmojiTextView
import autodagger.AutoInjector
import butterknife.BindView
import butterknife.ButterKnife
import coil.load
import com.google.android.flexbox.FlexboxLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
@ -53,57 +50,21 @@ import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewHolder<ChatMessage>(itemView) {
@JvmField
@BindView(R.id.messageText)
var messageText: EmojiTextView? = null
@JvmField
@BindView(R.id.messageTime)
var messageTimeView: TextView? = null
@JvmField
@BindView(R.id.quotedChatMessageView)
var quotedChatMessageView: RelativeLayout? = null
@JvmField
@BindView(R.id.quotedMessageAuthor)
var quotedUserName: EmojiTextView? = null
@JvmField
@BindView(R.id.quotedMessageImage)
var quotedMessagePreview: ImageView? = null
@JvmField
@BindView(R.id.quotedMessage)
var quotedMessage: EmojiTextView? = null
@JvmField
@BindView(R.id.quoteColoredView)
var quoteColoredView: View? = null
@JvmField
@BindView(R.id.checkMark)
var checkMark: ImageView? = null
private val binding: ItemCustomOutcomingTextMessageBinding = ItemCustomOutcomingTextMessageBinding.bind(itemView)
private val realView: View = itemView
@JvmField
@Inject
var context: Context? = null
private val realView: View
init {
ButterKnife.bind(this, itemView)
this.realView = itemView
}
override fun onBind(message: ChatMessage) {
super.onBind(message)
sharedApplication!!.componentApplication.inject(this)
val messageParameters: HashMap<String, HashMap<String, String>>? = message.messageParameters
var messageString: Spannable = SpannableString(message.text)
realView.isSelected = false
messageTimeView!!.setTextColor(context!!.resources.getColor(R.color.white60))
val layoutParams = messageTimeView!!.layoutParams as FlexboxLayout.LayoutParams
binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60))
val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
layoutParams.isWrapBefore = false
var textSize = context!!.resources.getDimension(R.dimen.chat_text_size)
if (messageParameters != null && messageParameters.size > 0) {
@ -115,7 +76,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
) || individualHashMap["type"] == "call"
) {
messageString = searchAndReplaceWithMentionSpan(
messageText!!.context,
binding.messageText.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
@ -136,7 +97,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
textSize = (textSize * 2.5).toFloat()
layoutParams.isWrapBefore = true
messageTimeView!!.setTextColor(context!!.resources.getColor(R.color.warm_grey_four))
binding.messageTime.setTextColor(context!!.resources.getColor(R.color.warm_grey_four))
realView.isSelected = true
}
val resources = sharedApplication!!.resources
@ -162,9 +123,9 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
)
ViewCompat.setBackground(bubble, bubbleDrawable)
}
messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
messageTimeView!!.layoutParams = layoutParams
messageText!!.text = messageString
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageTime.layoutParams = layoutParams
binding.messageText.text = messageString
// parent message handling
@ -172,27 +133,29 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
var parentChatMessage = message.parentMessage
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
quotedMessagePreview?.visibility = View.VISIBLE
quotedMessagePreview?.load(it) {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
)
}
} ?: run {
quotedMessagePreview?.visibility = View.GONE
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
quotedUserName?.text = parentChatMessage.actorDisplayName
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
quotedMessage?.text = parentChatMessage.text
quotedMessage?.setTextColor(context!!.resources.getColor(R.color.nc_outcoming_text_default))
quotedUserName?.setTextColor(context!!.resources.getColor(R.color.nc_grey))
binding.messageQuote.quotedMessage.text = parentChatMessage.text
binding.messageQuote.quotedMessage.setTextColor(
context!!.resources.getColor(R.color.nc_outcoming_text_default)
)
binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey))
quoteColoredView?.setBackgroundResource(R.color.white)
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
quotedChatMessageView?.visibility = View.VISIBLE
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
quotedChatMessageView?.visibility = View.GONE
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}
val readStatusDrawableInt = when (message.readStatus) {
@ -210,11 +173,11 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
readStatusDrawableInt?.let { drawableInt ->
context?.resources?.getDrawable(drawableInt, null)?.let {
it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP)
checkMark?.setImageDrawable(it)
binding.checkMark.setImageDrawable(it)
}
}
checkMark?.setContentDescription(readStatusContentDescriptionString)
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
}

View File

@ -3,8 +3,10 @@
*
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
* @author Andy Scherzinger
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* 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
@ -35,6 +37,7 @@ import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import com.google.common.util.concurrent.ListenableFuture;
import com.nextcloud.talk.R;
@ -70,8 +73,6 @@ import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import autodagger.AutoInjector;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.annotations.NonNull;
@ -82,15 +83,10 @@ import okhttp3.OkHttpClient;
import static com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback.REPLYABLE_VIEW_TAG;
@AutoInjector(NextcloudTalkApplication.class)
public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder<ChatMessage> {
public abstract class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder<ChatMessage> {
private static final String TAG = "PreviewMsgViewHolder";
@BindView(R.id.messageText)
EmojiTextView messageText;
View progressBar;
@Inject
Context context;
@ -99,8 +95,6 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
public MagicPreviewMessageViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
progressBar = itemView.findViewById(R.id.progress_bar);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
}
@ -131,7 +125,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
if (message.getMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) {
String fileName = message.getSelectedIndividualHashMap().get("name");
messageText.setText(fileName);
getMessageText().setText(fileName);
if (message.getSelectedIndividualHashMap().containsKey("mimetype")) {
String mimetype = message.getSelectedIndividualHashMap().get("mimetype");
int drawableResourceId = DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(mimetype);
@ -165,7 +159,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
try {
for (WorkInfo workInfo : workers.get()) {
if (workInfo.getState() == WorkInfo.State.RUNNING || workInfo.getState() == WorkInfo.State.ENQUEUED) {
progressBar.setVisibility(View.VISIBLE);
getProgressBar().setVisibility(View.VISIBLE);
String mimetype = message.getSelectedIndividualHashMap().get("mimetype");
@ -177,13 +171,12 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "Error when checking if worker already exists", e);
}
} else if (message.getMessageType() == ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) {
messageText.setText("GIPHY");
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText);
getMessageText().setText("GIPHY");
DisplayUtils.setClickableString("GIPHY", "https://giphy.com", getMessageText());
} else if (message.getMessageType() == ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) {
messageText.setText("Tenor");
DisplayUtils.setClickableString("Tenor", "https://tenor.com", messageText);
getMessageText().setText("Tenor");
DisplayUtils.setClickableString("Tenor", "https://tenor.com", getMessageText());
} else {
if (message.getMessageType().equals(ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE)) {
image.setOnClickListener(v -> {
@ -194,12 +187,16 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
} else {
image.setOnClickListener(null);
}
messageText.setText("");
getMessageText().setText("");
}
itemView.setTag(REPLYABLE_VIEW_TAG, message.isReplyable());
}
public abstract EmojiTextView getMessageText();
public abstract ProgressBar getProgressBar();
private void openOrDownloadFile(ChatMessage message) {
String filename = message.getSelectedIndividualHashMap().get("name");
String mimetype = message.getSelectedIndividualHashMap().get("mimetype");
@ -389,7 +386,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
WorkManager.getInstance().enqueue(downloadWorker);
progressBar.setVisibility(View.VISIBLE);
getProgressBar().setVisibility(View.VISIBLE);
WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.getId()).observeForever(workInfo -> {
updateViewsByProgress(fileName, mimetype, workInfo);
@ -401,7 +398,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
case RUNNING:
int progress = workInfo.getProgress().getInt(DownloadFileToCacheWorker.PROGRESS, -1);
if (progress > -1) {
messageText.setText(String.format(context.getResources().getString(R.string.filename_progress), fileName, progress));
getMessageText().setText(String.format(context.getResources().getString(R.string.filename_progress), fileName, progress));
}
break;
@ -412,13 +409,13 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
Log.d(TAG, "file " + fileName + " was downloaded but it's not opened because view is not shown on" +
" screen");
}
messageText.setText(fileName);
progressBar.setVisibility(View.GONE);
getMessageText().setText(fileName);
getProgressBar().setVisibility(View.GONE);
break;
case FAILED:
messageText.setText(fileName);
progressBar.setVisibility(View.GONE);
getMessageText().setText(fileName);
getProgressBar().setVisibility(View.GONE);
break;
default:
// do nothing
@ -487,6 +484,5 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
Log.e(TAG, "Error reading file information", e);
}
});
}
}

View File

@ -0,0 +1,250 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
*
* 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 com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.net.Uri
import android.util.Log
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.ViewCompat
import autodagger.AutoInjector
import coil.load
import com.google.android.flexbox.FlexboxLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.stfalcon.chatkit.messages.MessageHolders
import java.net.URLEncoder
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
.OutcomingTextMessageViewHolder<ChatMessage>(incomingView) {
private val binding: ItemCustomOutcomingLocationMessageBinding =
ItemCustomOutcomingLocationMessageBinding.bind(itemView)
private val realView: View = itemView
var locationLon: String? = ""
var locationLat: String? = ""
var locationName: String? = ""
var locationGeoLink: String? = ""
@JvmField
@Inject
var context: Context? = null
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
super.onBind(message)
sharedApplication!!.componentApplication.inject(this)
realView.isSelected = false
binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60))
val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
layoutParams.isWrapBefore = false
val textSize = context!!.resources.getDimension(R.dimen.chat_text_size)
colorizeMessageBubble(message)
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageTime.layoutParams = layoutParams
binding.messageText.text = message.text
binding.messageText.isEnabled = false
// parent message handling
setParentMessageDataOnMessageItem(message)
val readStatusDrawableInt = when (message.readStatus) {
ReadStatus.READ -> R.drawable.ic_check_all
ReadStatus.SENT -> R.drawable.ic_check
else -> null
}
val readStatusContentDescriptionString = when (message.readStatus) {
ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read)
ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent)
else -> null
}
readStatusDrawableInt?.let { drawableInt ->
AppCompatResources.getDrawable(context!!, drawableInt)?.let {
it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP)
binding.checkMark.setImageDrawable(it)
}
}
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
// geo-location
setLocationDataOnMessageItem(message)
}
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
private fun setLocationDataOnMessageItem(message: ChatMessage) {
if (message.messageParameters != null && message.messageParameters.size > 0) {
for (key in message.messageParameters.keys) {
val individualHashMap: Map<String, String> = message.messageParameters[key]!!
if (individualHashMap["type"] == "geo-location") {
locationLon = individualHashMap["longitude"]
locationLat = individualHashMap["latitude"]
locationName = individualHashMap["name"]
locationGeoLink = individualHashMap["id"]
}
}
}
binding.webview.settings?.javaScriptEnabled = true
binding.webview.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return if (url != null && (url.startsWith("http://") || url.startsWith("https://"))
) {
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
true
} else {
false
}
}
}
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
urlStringBuffer.append(
"?mapProviderUrl=" + URLEncoder.encode(context!!.getString(R.string.osm_tile_server_url))
)
urlStringBuffer.append(
"&mapProviderAttribution=" + URLEncoder.encode(
context!!.getString(
R.string
.osm_tile_server_attributation
)
)
)
urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat))
urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon))
urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName))
urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink))
binding.webview.loadUrl(urlStringBuffer.toString())
binding.webview.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_UP -> openGeoLink()
}
return v?.onTouchEvent(event) ?: true
}
})
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = parentChatMessage.text
binding.messageQuote.quotedMessage.setTextColor(
context!!.resources.getColor(R.color.nc_outcoming_text_default)
)
binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey))
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}
}
private fun colorizeMessageBubble(message: ChatMessage) {
val resources = sharedApplication!!.resources
val bgBubbleColor = if (message.isDeleted) {
resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted)
} else {
resources.getColor(R.color.bg_message_list_outcoming_bubble)
}
if (message.isGrouped) {
val bubbleDrawable = DisplayUtils.getMessageSelector(
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor,
R.drawable.shape_grouped_outcoming_message
)
ViewCompat.setBackground(bubble, bubbleDrawable)
} else {
val bubbleDrawable = DisplayUtils.getMessageSelector(
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor,
R.drawable.shape_outcoming_message
)
ViewCompat.setBackground(bubble, bubbleDrawable)
}
}
private fun openGeoLink() {
if (!locationGeoLink.isNullOrEmpty()) {
val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!)
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(geoLinkWithMarker))
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context!!.startActivity(browserIntent)
} else {
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
Log.e(TAG, "locationGeoLink was null or empty")
}
}
private fun addMarkerToGeoLink(locationGeoLink: String): String {
return locationGeoLink.replace("geo:", "geo:0,0?q=")
}
companion object {
private const val TAG = "LocOutMessageView"
}
}

View File

@ -0,0 +1,45 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
*
* 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 com.nextcloud.talk.adapters.messages;
import android.view.View;
import android.widget.ProgressBar;
import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
import androidx.emoji.widget.EmojiTextView;
public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder {
private final ItemCustomOutcomingPreviewMessageBinding binding;
public OutcomingPreviewMessageViewHolder(View itemView) {
super(itemView);
binding = ItemCustomOutcomingPreviewMessageBinding.bind(itemView);
}
public EmojiTextView getMessageText() {
return binding.messageText;
}
public ProgressBar getProgressBar() {
return binding.progressBar;
}
}

View File

@ -406,4 +406,12 @@ public interface NcApi {
@GET
Call<ResponseBody> downloadResizedImage(@Header("Authorization") String authorization,
@Url String url);
@FormUrlEncoded
@POST
Observable<GenericOverall> sendLocation(@Header("Authorization") String authorization,
@Url String url,
@Field("objectType") String objectType,
@Field("objectId") String objectId,
@Field("metaData") String metaData);
}

View File

@ -78,11 +78,14 @@ import com.facebook.imagepipeline.image.CloseableImage
import com.google.android.flexbox.FlexboxLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MagicCallActivity
import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
@ -133,6 +136,7 @@ import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.ImageLoader
import com.stfalcon.chatkit.commons.models.IMessage
import com.stfalcon.chatkit.messages.MessageHolders
import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
import com.stfalcon.chatkit.messages.MessagesListAdapter
import com.stfalcon.chatkit.utils.DateFormatter
import com.vanniktech.emoji.EmojiPopup
@ -163,7 +167,7 @@ class ChatController(args: Bundle) :
MessagesListAdapter.OnLoadMoreListener,
MessagesListAdapter.Formatter<Date>,
MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
MessageHolders.ContentChecker<IMessage> {
ContentChecker<ChatMessage> {
private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
@Inject
@ -397,17 +401,20 @@ class ChatController(args: Bundle) :
)
messageHolders.setIncomingImageConfig(
MagicPreviewMessageViewHolder::class.java,
IncomingPreviewMessageViewHolder::class.java,
R.layout.item_custom_incoming_preview_message
)
messageHolders.setOutcomingImageConfig(
MagicPreviewMessageViewHolder::class.java,
OutcomingPreviewMessageViewHolder::class.java,
R.layout.item_custom_outcoming_preview_message
)
messageHolders.registerContentType(
CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java,
R.layout.item_system_message, MagicSystemMessageViewHolder::class.java,
CONTENT_TYPE_SYSTEM_MESSAGE,
MagicSystemMessageViewHolder::class.java,
R.layout.item_system_message,
MagicSystemMessageViewHolder::class.java,
R.layout.item_system_message,
this
)
@ -420,6 +427,15 @@ class ChatController(args: Bundle) :
R.layout.item_date_header, this
)
messageHolders.registerContentType(
CONTENT_TYPE_LOCATION,
IncomingLocationMessageViewHolder::class.java,
R.layout.item_custom_incoming_location_message,
OutcomingLocationMessageViewHolder::class.java,
R.layout.item_custom_outcoming_location_message,
this
)
var senderId = ""
if (!conversationUser?.userId.equals("?")) {
senderId = "users/" + conversationUser?.userId
@ -793,6 +809,18 @@ class ChatController(args: Bundle) :
)
}
fun showShareLocationScreen() {
Log.d(TAG, "showShareLocationScreen")
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
router.pushController(
RouterTransaction.with(LocationPickerController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
private fun showConversationInfoScreen() {
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
@ -1861,6 +1889,8 @@ class ChatController(args: Bundle) :
if (message.hasFileAttachment()) return false
if (OBJECT_MESSAGE.equals(message.message)) return false
val isOlderThanSixHours = message
.createdAt
?.before(Date(System.currentTimeMillis() - AGE_THREHOLD_FOR_DELETE_MESSAGE)) == true
@ -1878,8 +1908,9 @@ class ChatController(args: Bundle) :
return true
}
override fun hasContentFor(message: IMessage, type: Byte): Boolean {
override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {
return when (type) {
CONTENT_TYPE_LOCATION -> return message.isLocationMessage()
CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
else -> false
@ -1985,6 +2016,7 @@ class ChatController(args: Bundle) :
private const val TAG = "ChatController"
private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
private const val CONTENT_TYPE_LOCATION: Byte = 3
private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
private const val LOBBY_TIMER_DELAY: Long = 5000
@ -1992,5 +2024,6 @@ class ChatController(args: Bundle) :
private const val MESSAGE_MAX_LENGTH: Int = 1000
private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
private const val OBJECT_MESSAGE: String = "{object}"
}
}

View File

@ -0,0 +1,220 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
*
* 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 com.nextcloud.talk.controllers
import android.app.SearchManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.AdapterView
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat
import androidx.preference.PreferenceManager
import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.GeocodingAdapter
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.base.NewBaseController
import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.databinding.ControllerGeocodingBinding
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.UserUtils
import fr.dudie.nominatim.client.TalkJsonNominatimClient
import fr.dudie.nominatim.model.Address
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.osmdroid.config.Configuration
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class GeocodingController(args: Bundle) :
NewBaseController(
R.layout.controller_geocoding,
args
),
SearchView.OnQueryTextListener {
private val binding: ControllerGeocodingBinding by viewBinding(ControllerGeocodingBinding::bind)
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var userUtils: UserUtils
@Inject
lateinit var okHttpClient: OkHttpClient
var roomToken: String?
var nominatimClient: TalkJsonNominatimClient? = null
var searchItem: MenuItem? = null
var searchView: SearchView? = null
var query: String? = null
lateinit var adapter: GeocodingAdapter
private var geocodingResults: List<Address> = ArrayList()
constructor(args: Bundle, listener: LocationPickerController) : this(args) {
targetController = listener
}
init {
setHasOptionsMenu(true)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
query = args.getString(BundleKeys.KEY_GEOCODING_QUERY)
roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN)
}
private fun initAdapter(addresses: List<Address>) {
adapter = GeocodingAdapter(binding.geocodingResults.context!!, addresses)
binding.geocodingResults.adapter = adapter
}
override fun onAttach(view: View) {
super.onAttach(view)
initAdapter(geocodingResults)
initGeocoder()
if (!query.isNullOrEmpty()) {
searchLocation()
} else {
Log.e(TAG, "search string that was passed to GeocodingController was null or empty")
}
binding.geocodingResults.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
val address: Address = adapter.getItem(position) as Address
val listener: GeocodingResultListener? = targetController as GeocodingResultListener?
listener?.receiveChosenGeocodingResult(address.latitude, address.longitude, address.displayName)
router.popCurrentController()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_geocoding, menu)
searchItem = menu.findItem(R.id.geocoding_action_search)
initSearchView()
searchItem?.expandActionView()
searchView?.setQuery(query, false)
searchView?.clearFocus()
}
override fun onQueryTextSubmit(query: String?): Boolean {
this.query = query
searchLocation()
searchView?.clearFocus()
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
private fun initSearchView() {
if (activity != null) {
val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
searchView?.maxWidth = Int.MAX_VALUE
searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
}
searchView?.imeOptions = imeOptions
searchView?.queryHint = resources!!.getString(R.string.nc_search)
searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
searchView?.setOnQueryTextListener(this)
searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
router.popCurrentController()
return true
}
})
}
}
}
private fun initGeocoder() {
val baseUrl = context!!.getString(R.string.osm_geocoder_url)
val email = context!!.getString(R.string.osm_geocoder_contact)
nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email)
}
private fun searchLocation(): Boolean {
CoroutineScope(IO).launch {
executeGeocodingRequest()
}
return true
}
@Suppress("Detekt.TooGenericExceptionCaught")
private suspend fun executeGeocodingRequest() {
var results: ArrayList<Address> = ArrayList()
try {
results = nominatimClient!!.search(query) as ArrayList<Address>
for (address in results) {
Log.d(TAG, address.displayName)
Log.d(TAG, address.latitude.toString())
Log.d(TAG, address.longitude.toString())
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get geocoded addresses", e)
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
}
updateResultsOnMainThread(results)
}
private suspend fun updateResultsOnMainThread(results: ArrayList<Address>) {
withContext(Main) {
initAdapter(results)
}
}
interface GeocodingResultListener {
fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String)
}
companion object {
private const val TAG = "GeocodingController"
}
}

View File

@ -0,0 +1,492 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
*
* 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 com.nextcloud.talk.controllers
import android.Manifest
import android.app.SearchManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.core.content.PermissionChecker
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.MenuItemCompat
import androidx.preference.PreferenceManager
import autodagger.AutoInjector
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.base.NewBaseController
import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.databinding.ControllerLocationBinding
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.database.user.UserUtils
import fr.dudie.nominatim.client.TalkJsonNominatimClient
import fr.dudie.nominatim.model.Address
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.osmdroid.config.Configuration.getInstance
import org.osmdroid.events.DelayedMapListener
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.overlay.CopyrightOverlay
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class LocationPickerController(args: Bundle) :
NewBaseController(
R.layout.controller_location,
args
),
SearchView.OnQueryTextListener,
LocationListener,
GeocodingController.GeocodingResultListener {
private val binding: ControllerLocationBinding by viewBinding(ControllerLocationBinding::bind)
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var userUtils: UserUtils
@Inject
lateinit var okHttpClient: OkHttpClient
var nominatimClient: TalkJsonNominatimClient? = null
var roomToken: String?
var myLocation: GeoPoint = GeoPoint(0.0, 0.0)
private var locationManager: LocationManager? = null
private lateinit var locationOverlay: MyLocationNewOverlay
var moveToCurrentLocationWasClicked: Boolean = true
var readyToShareLocation: Boolean = false
var searchItem: MenuItem? = null
var searchView: SearchView? = null
var receivedChosenGeocodingResult: Boolean = false
var geocodedLat: Double = 0.0
var geocodedLon: Double = 0.0
var geocodedName: String = ""
init {
setHasOptionsMenu(true)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
roomToken = args.getString(KEY_ROOM_TOKEN)
}
override fun onAttach(view: View) {
super.onAttach(view)
initMap()
}
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onDetach(view: View) {
super.onDetach(view)
try {
locationManager!!.removeUpdates(this)
} catch (e: Exception) {
Log.e(TAG, "error when trying to remove updates for location Manager", e)
}
locationOverlay.disableMyLocation()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_locationpicker, menu)
searchItem = menu.findItem(R.id.location_action_search)
initSearchView()
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
actionBar?.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent)))
actionBar?.title = context!!.getString(R.string.nc_share_location)
}
override val title: String
get() =
resources!!.getString(R.string.nc_share_location)
override fun onViewBound(view: View) {
setLocationDescription(false, receivedChosenGeocodingResult)
binding.shareLocation.isClickable = false
binding.shareLocation.setOnClickListener {
if (readyToShareLocation) {
shareLocation(
binding.map.mapCenter?.latitude,
binding.map.mapCenter?.longitude,
binding.placeName.text.toString()
)
} else {
Log.w(TAG, "readyToShareLocation was false while user tried to share location.")
}
}
}
private fun initSearchView() {
if (activity != null) {
val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
searchView?.maxWidth = Int.MAX_VALUE
searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
}
searchView?.imeOptions = imeOptions
searchView?.queryHint = resources!!.getString(R.string.nc_search)
searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
searchView?.setOnQueryTextListener(this)
}
}
}
override fun onQueryTextSubmit(query: String?): Boolean {
if (!query.isNullOrEmpty()) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_GEOCODING_QUERY, query)
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
router.pushController(
RouterTransaction.with(GeocodingController(bundle, this))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
private fun initMap() {
if (!isLocationPermissionsGranted()) {
requestLocationPermissions()
}
binding.map.setTileSource(TileSourceFactory.MAPNIK)
binding.map.onResume()
locationManager = activity!!.getSystemService(Context.LOCATION_SERVICE) as LocationManager
try {
locationManager!!.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0f, this)
} catch (ex: SecurityException) {
Log.w(TAG, "Error requesting location updates", ex)
}
val copyrightOverlay = CopyrightOverlay(context)
binding.map.overlays?.add(copyrightOverlay)
binding.map.setMultiTouchControls(true)
binding.map.isTilesScaledToDpi = true
locationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map)
locationOverlay.enableMyLocation()
locationOverlay.setPersonHotspot(PERSON_HOT_SPOT_X, PERSON_HOT_SPOT_Y)
locationOverlay.setPersonIcon(
DisplayUtils.getBitmap(
ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null)
)
)
binding.map.overlays?.add(locationOverlay)
val mapController = binding.map.controller
if (receivedChosenGeocodingResult) {
mapController?.setZoom(ZOOM_LEVEL_RECEIVED_RESULT)
} else {
mapController?.setZoom(ZOOM_LEVEL_DEFAULT)
}
val zoomToCurrentPositionOnFirstFix = !receivedChosenGeocodingResult
locationOverlay.runOnFirstFix {
myLocation = locationOverlay.myLocation
if (zoomToCurrentPositionOnFirstFix) {
activity!!.runOnUiThread {
mapController?.setZoom(ZOOM_LEVEL_DEFAULT)
mapController?.setCenter(myLocation)
}
}
}
if (receivedChosenGeocodingResult && geocodedLat != GEOCODE_ZERO && geocodedLon != GEOCODE_ZERO) {
mapController?.setCenter(GeoPoint(geocodedLat, geocodedLon))
}
binding.centerMapButton.setOnClickListener {
mapController?.animateTo(myLocation)
moveToCurrentLocationWasClicked = true
}
binding.map.addMapListener(
DelayedMapListener(
object : MapListener {
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onScroll(paramScrollEvent: ScrollEvent): Boolean {
try {
when {
moveToCurrentLocationWasClicked -> {
setLocationDescription(isGpsLocation = true, isGeocodedResult = false)
moveToCurrentLocationWasClicked = false
}
receivedChosenGeocodingResult -> {
binding.shareLocation.isClickable = true
setLocationDescription(isGpsLocation = false, isGeocodedResult = true)
receivedChosenGeocodingResult = false
}
else -> {
binding.shareLocation.isClickable = true
setLocationDescription(isGpsLocation = false, isGeocodedResult = false)
}
}
} catch (e: NullPointerException) {
Log.d(TAG, "UI already closed")
}
readyToShareLocation = true
return true
}
override fun onZoom(event: ZoomEvent): Boolean {
return false
}
})
)
}
private fun setLocationDescription(isGpsLocation: Boolean, isGeocodedResult: Boolean) {
when {
isGpsLocation -> {
binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_current_location)
binding.placeName.visibility = View.GONE
binding.placeName.text = ""
}
isGeocodedResult -> {
binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location)
binding.placeName.visibility = View.VISIBLE
binding.placeName.text = geocodedName
}
else -> {
binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location)
binding.placeName.visibility = View.GONE
binding.placeName.text = ""
}
}
}
private fun shareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) {
if (selectedLat != null || selectedLon != null) {
val name = locationName
if (name.isNullOrEmpty()) {
initGeocoder()
searchPlaceNameForCoordinates(selectedLat!!, selectedLon!!)
} else {
executeShareLocation(selectedLat, selectedLon, locationName)
}
}
}
private fun executeShareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) {
val objectId = "geo:$selectedLat,$selectedLon"
val metaData: String =
"{\"type\":\"geo-location\",\"id\":\"geo:$selectedLat,$selectedLon\",\"latitude\":\"$selectedLat\"," +
"\"longitude\":\"$selectedLon\",\"name\":\"$locationName\"}"
ncApi.sendLocation(
ApiUtils.getCredentials(userUtils.currentUser?.username, userUtils.currentUser?.token),
ApiUtils.getUrlToSendLocation(userUtils.currentUser?.baseUrl, roomToken),
"geo-location",
objectId,
metaData
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(t: GenericOverall) {
router.popCurrentController()
}
override fun onError(e: Throwable) {
Log.e(TAG, "error when trying to share location", e)
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
router.popCurrentController()
}
override fun onComplete() {
// unused atm
}
})
}
private fun isLocationPermissionsGranted(): Boolean {
fun isCoarseLocationGranted(): Boolean {
return PermissionChecker.checkSelfPermission(
context!!,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PermissionChecker.PERMISSION_GRANTED
}
fun isFineLocationGranted(): Boolean {
return PermissionChecker.checkSelfPermission(
context!!,
Manifest.permission.ACCESS_FINE_LOCATION
) == PermissionChecker.PERMISSION_GRANTED
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
isCoarseLocationGranted() && isFineLocationGranted()
} else {
true
}
}
private fun requestLocationPermissions() {
requestPermissions(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
),
REQUEST_PERMISSIONS_REQUEST_CODE
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE &&
grantResults.size > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
initMap()
} else {
Toast.makeText(context, context!!.getString(R.string.nc_location_permission_required), Toast.LENGTH_LONG)
.show()
}
}
override fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String) {
receivedChosenGeocodingResult = true
geocodedLat = lat
geocodedLon = lon
geocodedName = name
}
private fun initGeocoder() {
val baseUrl = context!!.getString(R.string.osm_geocoder_url)
val email = context!!.getString(R.string.osm_geocoder_contact)
nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email)
}
private fun searchPlaceNameForCoordinates(lat: Double, lon: Double): Boolean {
CoroutineScope(Dispatchers.IO).launch {
executeGeocodingRequest(lat, lon)
}
return true
}
@Suppress("Detekt.TooGenericExceptionCaught")
private suspend fun executeGeocodingRequest(lat: Double, lon: Double) {
var address: Address? = null
try {
address = nominatimClient!!.getAddress(lon, lat)
} catch (e: Exception) {
Log.e(TAG, "Failed to get geocoded addresses", e)
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
}
updateResultOnMainThread(lat, lon, address?.displayName)
}
private suspend fun updateResultOnMainThread(lat: Double, lon: Double, addressName: String?) {
withContext(Dispatchers.Main) {
executeShareLocation(lat, lon, addressName)
}
}
override fun onLocationChanged(location: Location?) {
myLocation = GeoPoint(location)
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
// empty
}
override fun onProviderEnabled(provider: String?) {
// empty
}
override fun onProviderDisabled(provider: String?) {
// empty
}
companion object {
private const val TAG = "LocPicker"
private const val REQUEST_PERMISSIONS_REQUEST_CODE = 1
private const val PERSON_HOT_SPOT_X: Float = 20.0F
private const val PERSON_HOT_SPOT_Y: Float = 20.0F
private const val ZOOM_LEVEL_RECEIVED_RESULT: Double = 14.0
private const val ZOOM_LEVEL_DEFAULT: Double = 14.0
private const val GEOCODE_ZERO: Double = 0.0
}
}

View File

@ -0,0 +1,28 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
*
* 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 com.nextcloud.talk.interfaces
import com.stfalcon.chatkit.commons.models.IMessage
interface ExtendedIMessage : IMessage {
fun isLocationMessage(): Boolean
}

View File

@ -26,27 +26,30 @@ import com.bluelinelabs.logansquare.annotation.JsonIgnore;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.interfaces.ExtendedIMessage;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.TextMatchers;
import com.stfalcon.chatkit.commons.models.IMessage;
import com.stfalcon.chatkit.commons.models.IUser;
import com.stfalcon.chatkit.commons.models.MessageContentType;
import org.parceler.Parcel;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import androidx.annotation.Nullable;
import kotlin.text.Charsets;
@Parcel
@JsonObject
public class ChatMessage implements IMessage, MessageContentType, MessageContentType.Image {
public class ChatMessage implements ExtendedIMessage, MessageContentType, MessageContentType.Image {
@JsonIgnore
public boolean isGrouped;
@JsonIgnore
@ -87,15 +90,21 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
public Enum<ReadStatus> readStatus = ReadStatus.NONE;
@JsonIgnore
List<MessageType> messageTypesToIgnore = Arrays.asList(MessageType.REGULAR_TEXT_MESSAGE,
MessageType.SYSTEM_MESSAGE, MessageType.SINGLE_LINK_VIDEO_MESSAGE,
MessageType.SINGLE_LINK_AUDIO_MESSAGE, MessageType.SINGLE_LINK_MESSAGE);
List<MessageType> messageTypesToIgnore = Arrays.asList(
MessageType.REGULAR_TEXT_MESSAGE,
MessageType.SYSTEM_MESSAGE,
MessageType.SINGLE_LINK_VIDEO_MESSAGE,
MessageType.SINGLE_LINK_AUDIO_MESSAGE,
MessageType.SINGLE_LINK_MESSAGE,
MessageType.SINGLE_NC_GEOLOCATION_MESSAGE);
public boolean hasFileAttachment() {
if (messageParameters != null && messageParameters.size() > 0) {
for (String key : messageParameters.keySet()) {
Map<String, String> individualHashMap = messageParameters.get(key);
if (individualHashMap.get("type").equals("file")) {
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
Map<String, String> individualHashMap = entry.getValue();
if(MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("file").getBytes(Charsets.UTF_8))) {
return true;
}
}
@ -103,13 +112,31 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
return false;
}
private boolean hasGeoLocation() {
if (messageParameters != null && messageParameters.size() > 0) {
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
Map<String, String> individualHashMap = entry.getValue();
if(MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("geo-location").getBytes(Charsets.UTF_8))) {
return true;
}
}
}
return false;
}
@Nullable
@Override
public String getImageUrl() {
if (messageParameters != null && messageParameters.size() > 0) {
for (String key : messageParameters.keySet()) {
Map<String, String> individualHashMap = messageParameters.get(key);
if (individualHashMap.get("type").equals("file")) {
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
Map<String, String> individualHashMap = entry.getValue();
if(MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("file").getBytes(Charsets.UTF_8))) {
selectedIndividualHashMap = individualHashMap;
return (ApiUtils.getUrlForFilePreviewWithFileId(getActiveUser().getBaseUrl(),
individualHashMap.get("id"), NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size)));
@ -133,6 +160,11 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
return MessageType.SINGLE_NC_ATTACHMENT_MESSAGE;
}
if (hasGeoLocation()) {
return MessageType.SINGLE_NC_GEOLOCATION_MESSAGE;
}
return TextMatchers.getMessageTypeFromString(getText());
}
@ -158,22 +190,29 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
if (getMessageType().equals(MessageType.REGULAR_TEXT_MESSAGE) || getMessageType().equals(MessageType.SYSTEM_MESSAGE) || getMessageType().equals(MessageType.SINGLE_LINK_MESSAGE)) {
return getText();
} else {
if (getMessageType().equals(MessageType.SINGLE_LINK_GIPHY_MESSAGE)
|| getMessageType().equals(MessageType.SINGLE_LINK_TENOR_MESSAGE)
|| getMessageType().equals(MessageType.SINGLE_LINK_GIF_MESSAGE)) {
if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getMessageType()
|| MessageType.SINGLE_LINK_TENOR_MESSAGE == getMessageType()
|| MessageType.SINGLE_LINK_GIF_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_gif_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_gif),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (getMessageType().equals(MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)) {
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_attachment_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_attachment),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_location_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_location),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
/*} else if (getMessageType().equals(MessageType.SINGLE_LINK_MESSAGE)) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_link_you));
@ -181,21 +220,21 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_link),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}*/
} else if (getMessageType().equals(MessageType.SINGLE_LINK_AUDIO_MESSAGE)) {
} else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_audio_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_audio),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (getMessageType().equals(MessageType.SINGLE_LINK_VIDEO_MESSAGE)) {
} else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_video_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_video),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (getMessageType().equals(MessageType.SINGLE_LINK_IMAGE_MESSAGE)) {
} else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_image_you));
} else {
@ -537,6 +576,11 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
return "ChatMessage(isGrouped=" + this.isGrouped() + ", isOneToOneConversation=" + this.isOneToOneConversation() + ", activeUser=" + this.getActiveUser() + ", selectedIndividualHashMap=" + this.getSelectedIndividualHashMap() + ", isLinkPreviewAllowed=" + this.isLinkPreviewAllowed() + ", isDeleted=" + this.isDeleted() + ", jsonMessageId=" + this.getJsonMessageId() + ", token=" + this.getToken() + ", actorType=" + this.getActorType() + ", actorId=" + this.getActorId() + ", actorDisplayName=" + this.getActorDisplayName() + ", timestamp=" + this.getTimestamp() + ", message=" + this.getMessage() + ", messageParameters=" + this.getMessageParameters() + ", systemMessageType=" + this.getSystemMessageType() + ", replyable=" + this.isReplyable() + ", parentMessage=" + this.getParentMessage() + ", readStatus=" + this.getReadStatus() + ", messageTypesToIgnore=" + this.getMessageTypesToIgnore() + ")";
}
@Override
public boolean isLocationMessage() {
return hasGeoLocation();
}
public enum MessageType {
REGULAR_TEXT_MESSAGE,
SYSTEM_MESSAGE,
@ -548,6 +592,7 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
SINGLE_LINK_IMAGE_MESSAGE,
SINGLE_LINK_AUDIO_MESSAGE,
SINGLE_NC_ATTACHMENT_MESSAGE,
SINGLE_NC_GEOLOCATION_MESSAGE,
}
public enum SystemMessageType {

View File

@ -33,6 +33,8 @@ class ChatUtils {
val type = individualHashMap?.get("type")
if (type == "user" || type == "guest" || type == "call") {
resultMessage = resultMessage?.replace("{$key}", "@" + individualHashMap["name"])
} else if (type == "geo-location") {
resultMessage = individualHashMap.get("name")
} else if (individualHashMap?.containsKey("link") == true) {
resultMessage = if (type == "file") {
resultMessage?.replace("{$key}", individualHashMap["name"].toString())

View File

@ -2,7 +2,7 @@
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2021 Marcel Hibbe <marcel.hibbe@nextcloud.com>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
*
* 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
@ -22,7 +22,9 @@ package com.nextcloud.talk.ui.dialog
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatTextView
import butterknife.BindView
import butterknife.ButterKnife
@ -35,6 +37,14 @@ import com.nextcloud.talk.models.database.CapabilitiesUtil
class AttachmentDialog(val activity: Activity, var chatController: ChatController) : BottomSheetDialog(activity) {
@BindView(R.id.menu_share_location)
@JvmField
var shareLocationItem: LinearLayout? = null
@BindView(R.id.txt_share_location)
@JvmField
var shareLocation: AppCompatTextView? = null
@BindView(R.id.txt_attach_file_from_local)
@JvmField
var attachFromLocal: AppCompatTextView? = null
@ -60,6 +70,19 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
String.format(it.getString(R.string.nc_upload_from_cloud), serverName)
}
if (!CapabilitiesUtil.hasSpreedFeatureCapability(
chatController.conversationUser,
"geo-location-sharing"
)
) {
shareLocationItem?.visibility = View.GONE
}
shareLocation?.setOnClickListener {
chatController.showShareLocationScreen()
dismiss()
}
attachFromLocal?.setOnClickListener {
chatController.sendSelectLocalFileIntent()
dismiss()

View File

@ -389,4 +389,8 @@ public class ApiUtils {
public static String getUrlForUserFields(String baseUrl) {
return baseUrl + ocsApiVersion + "/cloud/user/fields";
}
public static String getUrlToSendLocation(String baseUrl, String roomToken) {
return baseUrl + ocsApiVersion + "/apps/spreed/api/v1/chat/" + roomToken + "/share";
}
}

View File

@ -55,19 +55,6 @@ import android.view.Window;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.XmlRes;
import androidx.appcompat.widget.AppCompatDrawableManager;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.emoji.text.EmojiCompat;
import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
@ -102,6 +89,19 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.XmlRes;
import androidx.appcompat.widget.AppCompatDrawableManager;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.emoji.text.EmojiCompat;
public class DisplayUtils {
private static final String TAG = "DisplayUtils";
@ -160,7 +160,7 @@ public class DisplayUtils {
return new BitmapDrawable(getRoundedBitmapFromVectorDrawableResource(resources, resource));
}
private static Bitmap getBitmap(Drawable drawable) {
public static Bitmap getBitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);

View File

@ -66,4 +66,5 @@ object BundleKeys {
val KEY_FILE_ID = "KEY_FILE_ID"
val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"
val KEY_SHARED_TEXT = "KEY_SHARED_TEXT"
val KEY_GEOCODING_QUERY = "KEY_GEOCODING_QUERY"
}

View File

@ -0,0 +1,312 @@
package fr.dudie.nominatim.client;
/*
* [license]
* Nominatim Java API client
* ~~~~
* Copyright (C) 2010 - 2014 Dudie
* ~~~~
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/lgpl-3.0.html>.
* [/license]
*/
import android.util.Log;
import com.github.filosganga.geogson.gson.GeometryAdapterFactory;
import com.github.filosganga.geogson.jts.JtsAdapterFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import fr.dudie.nominatim.client.request.NominatimLookupRequest;
import fr.dudie.nominatim.client.request.NominatimReverseRequest;
import fr.dudie.nominatim.client.request.NominatimSearchRequest;
import fr.dudie.nominatim.client.request.paramhelper.OsmType;
import fr.dudie.nominatim.gson.ArrayOfAddressElementsDeserializer;
import fr.dudie.nominatim.gson.ArrayOfPolygonPointsDeserializer;
import fr.dudie.nominatim.gson.BoundingBoxDeserializer;
import fr.dudie.nominatim.gson.PolygonPointDeserializer;
import fr.dudie.nominatim.model.Address;
import fr.dudie.nominatim.model.BoundingBox;
import fr.dudie.nominatim.model.Element;
import fr.dudie.nominatim.model.PolygonPoint;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* An implementation of the Nominatim Api Service.
*
* @author Jérémie Huchet
* @author Sunil D S
* @author Andy Scherzinger
*/
public final class TalkJsonNominatimClient implements NominatimClient {
private static final String TAG = "TalkNominationClient";
/**
* UTF-8 encoding.
*/
public static final String ENCODING_UTF_8 = "UTF-8";
private final OkHttpClient httpClient;
/**
* Gson instance for Nominatim API calls.
*/
private final Gson gson;
/**
* The url to make search queries.
*/
private final String searchUrl;
/**
* The url for reverse geocoding.
*/
private final String reverseUrl;
/**
* The url for address lookup.
*/
private final String lookupUrl;
/**
* The default search options.
*/
private final NominatimOptions defaults;
/**
* Creates the json nominatim client.
*
* @param baseUrl the nominatim server url
* @param httpClient an HTTP client
* @param email an email to add in the HTTP requests parameters to "sign" them (see
* https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy)
*/
public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email) {
this(baseUrl, httpClient, email, new NominatimOptions());
}
/**
* Creates the json nominatim client.
*
* @param baseUrl the nominatim server url
* @param httpClient an HTTP client
* @param email an email to add in the HTTP requests parameters to "sign" them (see
* https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy)
* @param defaults defaults options, they override null valued requests options
*/
public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email, final NominatimOptions defaults) {
String emailEncoded;
try {
emailEncoded = URLEncoder.encode(email, ENCODING_UTF_8);
} catch (UnsupportedEncodingException e) {
emailEncoded = email;
}
this.searchUrl = String.format("%s/search?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded);
this.reverseUrl = String.format("%s/reverse?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded);
this.lookupUrl = String.format("%s/lookup?format=json&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded);
Log.d(TAG, "API search URL: " + searchUrl);
Log.d(TAG, "API reverse URL: " + reverseUrl);
this.defaults = defaults;
// prepare gson instance
final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Element[].class, new ArrayOfAddressElementsDeserializer());
gsonBuilder.registerTypeAdapter(PolygonPoint.class, new PolygonPointDeserializer());
gsonBuilder.registerTypeAdapter(PolygonPoint[].class, new ArrayOfPolygonPointsDeserializer());
gsonBuilder.registerTypeAdapter(BoundingBox.class, new BoundingBoxDeserializer());
gsonBuilder.registerTypeAdapterFactory(new JtsAdapterFactory());
gsonBuilder.registerTypeAdapterFactory(new GeometryAdapterFactory());
gson = gsonBuilder.create();
// prepare httpclient
this.httpClient = httpClient;
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#search(fr.dudie.nominatim.client.request.NominatimSearchRequest)
*/
@Override
public List<Address> search(final NominatimSearchRequest search) throws IOException {
defaults.mergeTo(search);
final String apiCall = String.format("%s&%s", searchUrl, search.getQueryString());
Log.d(TAG, "search url: " + apiCall);
Request requesthttp = new Request.Builder()
.addHeader("accept", "application/json")
.url(apiCall)
.build();
Response response = httpClient.newCall(requesthttp).execute();
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
if (responseBody != null) {
return gson.fromJson(responseBody.string(), new TypeToken<List<Address>>() {
}.getType());
}
}
return new ArrayList<>();
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(fr.dudie.nominatim.client.request.NominatimReverseRequest)
*/
@Override
public Address getAddress(final NominatimReverseRequest reverse) throws IOException {
final String apiCall = String.format("%s&%s", reverseUrl, reverse.getQueryString());
Log.d(TAG, "reverse geocoding url: " + apiCall);
Request requesthttp = new Request.Builder()
.addHeader("accept", "application/json")
.url(apiCall)
.build();
Response response = httpClient.newCall(requesthttp).execute();
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
if (responseBody != null) {
return gson.fromJson(responseBody.string(), Address.class);
}
}
return null;
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(fr.dudie.nominatim.client.request.NominatimLookupRequest)
*/
@Override
public List<Address> lookupAddress(final NominatimLookupRequest lookup) throws IOException {
final String apiCall = String.format("%s&%s", lookupUrl, lookup.getQueryString());
Log.d(TAG, "lookup url: " + apiCall);
Request requesthttp = new Request.Builder()
.addHeader("accept", "application/json")
.url(apiCall)
.build();
Response response = httpClient.newCall(requesthttp).execute();
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
if (responseBody != null) {
return gson.fromJson(responseBody.string(), new TypeToken<List<Address>>() {
}.getType());
}
}
return new ArrayList<>();
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#search(java.lang.String)
*/
@Override
public List<Address> search(final String query) throws IOException {
final NominatimSearchRequest q = new NominatimSearchRequest();
q.setQuery(query);
return this.search(q);
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double)
*/
@Override
public Address getAddress(final double longitude, final double latitude) throws IOException {
final NominatimReverseRequest q = new NominatimReverseRequest();
q.setQuery(longitude, latitude);
return this.getAddress(q);
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double, int)
*/
@Override
public Address getAddress(final double longitude, final double latitude, final int zoom)
throws IOException {
final NominatimReverseRequest q = new NominatimReverseRequest();
q.setQuery(longitude, latitude);
q.setZoom(zoom);
return this.getAddress(q);
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(int, int)
*/
@Override
public Address getAddress(final int longitudeE6, final int latitudeE6) throws IOException {
return this.getAddress((longitudeE6 / 1E6), (latitudeE6 / 1E6));
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#getAddress(String, long)
*/
@Override
public Address getAddress(final String type, final long id) throws IOException {
final NominatimReverseRequest q = new NominatimReverseRequest();
q.setQuery(OsmType.from(type), id);
return this.getAddress(q);
}
/**
* {@inheritDoc}
*
* @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(java.util.List)
*/
@Override
public List<Address> lookupAddress(final List<String> typeId) throws IOException {
final NominatimLookupRequest q = new NominatimLookupRequest();
q.setQuery(typeId);
return this.lookupAddress(q);
}
}

View File

@ -0,0 +1,34 @@
<!--
~ Nextcloud Talk application
~
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#2C2C2C"
android:fillType="nonZero"
android:pathData="M12,0C5.4168,0 0,5.4168 0,12C0,18.5832 5.4168,24 12,24C18.5832,24 24,18.5832 24,12C24,5.4168 18.5832,0 12,0Z" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="M12,11.6989C11.1743,11.6989 10.4943,11.0189 10.4943,10.1932C10.4943,9.3675 11.1743,8.6875 12,8.6875C12.8257,8.6875 13.5057,9.3675 13.5057,10.1932C13.5057,11.0189 12.8257,11.6989 12,11.6989M12,5.9774C9.6873,5.9774 7.7842,7.8805 7.7842,10.1932C7.7842,13.3551 12,18.0226 12,18.0226C12,18.0226 16.2158,13.3551 16.2158,10.1932C16.2158,7.8805 14.3127,5.9774 12,5.9774Z" />
</vector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="#0082C9"/>
<stroke android:color="#FFFFFF" android:width="2dp"/>
<size android:width="15dp" android:height="15dp"/>
</shape>
</item>
</selector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/fontAppbar"
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#757575"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF0000"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>

View File

@ -0,0 +1,34 @@
<!--
~ Nextcloud Talk application
~
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#DBDBDB"
android:fillType="nonZero"
android:pathData="M12,0C5.4168,0 0,5.4168 0,12C0,18.5832 5.4168,24 12,24C18.5832,24 24,18.5832 24,12C24,5.4168 18.5832,0 12,0Z" />
<path
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="M12,11.6989C11.1743,11.6989 10.4943,11.0189 10.4943,10.1932C10.4943,9.3675 11.1743,8.6875 12,8.6875C12.8257,8.6875 13.5057,9.3675 13.5057,10.1932C13.5057,11.0189 12.8257,11.6989 12,11.6989M12,5.9774C9.6873,5.9774 7.7842,7.8805 7.7842,10.1932C7.7842,13.3551 12,18.0226 12,18.0226C12,18.0226 16.2158,13.3551 16.2158,10.1932C16.2158,7.8805 14.3127,5.9774 12,5.9774Z" />
</vector>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
~
~ 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parent_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/geocoding_results"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
~
~ 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parent_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<org.osmdroid.views.MapView
android:id="@+id/map"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/centerMapButton"
style="@style/Widget.AppTheme.Button.IconButton"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_margin="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:backgroundTint="@color/bg_default_semitransparent"
app:cornerRadius="@dimen/button_corner_radius"
app:elevation="0dp"
app:icon="@drawable/ic_baseline_gps_fixed_24"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="24dp"
app:iconTint="@color/high_emphasis_text" />
<View
android:id="@+id/locationpicker_anchor"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_centerInParent="true" />
<ImageView
android:layout_width="30dp"
android:layout_height="50dp"
android:layout_above="@id/locationpicker_anchor"
android:layout_centerHorizontal="true"
android:layout_marginBottom="-10dp"
android:contentDescription="@string/nc_location_current_position_description"
android:src="@drawable/ic_baseline_location_on_red_24" />
</RelativeLayout>
<LinearLayout
android:id="@+id/share_location"
android:layout_width="match_parent"
android:layout_height="72dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/roundedImageView"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:layout_gravity="top"
android:layout_margin="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_circular_location" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/share_location_description"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:textColor="@color/high_emphasis_text"
android:textSize="16sp"
tools:text="Share this location" />
<TextView
android:id="@+id/place_name"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/medium_emphasis_text"
android:textSize="14sp"
tools:text="Brandenburg, Germany" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -38,6 +38,38 @@
android:textColor="@color/medium_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
<LinearLayout
android:id="@+id/menu_share_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:paddingLeft="@dimen/standard_padding"
android:paddingTop="@dimen/standard_half_padding"
android:paddingRight="@dimen/standard_padding"
android:paddingBottom="@dimen/standard_half_padding"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_share_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_baseline_location_on_24"
app:tint="@color/colorPrimary" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_share_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/standard_margin"
android:text="@string/nc_share_location"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/menu_attach_file_from_local"
android:layout_width="match_parent"

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
~
~ 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="56dp"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/roundedImageView"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:layout_gravity="top"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_double_padding"
android:layout_marginBottom="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_circular_location" />
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/geocoding_result_text_size"
android:paddingTop="12dp"
android:paddingBottom="@dimen/standard_padding"
android:paddingStart="@dimen/standard_half_padding"
android:paddingEnd="@dimen/standard_padding"
tools:text="S Sonnenallee, 50, Saalestraße, Rixdorf, Neukölln, Berlin, 12055, Deutschland" />
</LinearLayout>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Marcel Hibbe
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
~ 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/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="2dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="2dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@id/messageUserAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp"
app:roundAsCircle="true" />
<com.google.android.flexbox.FlexboxLayout
android:id="@id/bubble"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/message_incoming_bubble_margin_right"
android:layout_toEndOf="@id/messageUserAvatar"
android:orientation="vertical"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<include
android:id="@+id/message_quote"
layout="@layout/item_message_quote"
android:visibility="gone" />
<WebView
android:id="@+id/webview"
android:layout_width="400dp"
android:layout_height="200dp" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/messageAuthor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:textColor="@color/textColorMaxContrast"
android:textSize="12sp" />
<androidx.emoji.widget.EmojiTextView
android:id="@id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true" />
<TextView
android:id="@id/messageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/messageText"
android:layout_marginStart="8dp"
app:layout_alignSelf="center" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -2,6 +2,8 @@
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
@ -47,7 +49,10 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
<include layout="@layout/item_message_quote" android:visibility="gone"/>
<include
android:id="@+id/message_quote"
layout="@layout/item_message_quote"
android:visibility="gone" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/messageAuthor"

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
~ 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/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="2dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="2dp">
<com.google.android.flexbox.FlexboxLayout
android:id="@id/bubble"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<include
android:id="@+id/message_quote"
layout="@layout/item_message_quote"
android:visibility="gone" />
<WebView
android:id="@+id/webview"
android:layout_width="400dp"
android:layout_height="200dp" />
<androidx.emoji.widget.EmojiTextView
android:id="@id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:lineSpacingMultiplier="1.2"
android:textColorHighlight="@color/nc_grey"
android:textIsSelectable="true"
tools:text="Talk to ayou later!" />
<TextView
android:id="@id/messageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/messageText"
android:layout_marginStart="8dp"
app:layout_alignSelf="center"
tools:text="10:35" />
<ImageView
android:id="@+id/checkMark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/messageTime"
android:layout_marginStart="8dp"
app:layout_alignSelf="center"
android:contentDescription="@null" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -41,7 +41,10 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
<include layout="@layout/item_message_quote" android:visibility="gone"/>
<include
android:id="@+id/message_quote"
layout="@layout/item_message_quote"
android:visibility="gone" />
<androidx.emoji.widget.EmojiTextView
android:id="@id/messageText"

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017 Mario Danic
~
~ 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/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Search, should appear as action button -->
<item android:id="@+id/geocoding_action_search"
android:title="@string/nc_search"
android:icon="@drawable/ic_search_white_24dp"
app:showAsAction="collapseActionView|always"
android:animateLayoutChanges="true"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017 Mario Danic
~
~ 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/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Search, should appear as action button -->
<item android:id="@+id/location_action_search"
android:title="@string/nc_search"
android:icon="@drawable/ic_search_white_24dp"
app:showAsAction="collapseActionView|always"
android:animateLayoutChanges="true"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View File

@ -39,6 +39,7 @@
<color name="low_emphasis_text">#61ffffff</color>
<color name="bg_default">#121212</color>
<color name="bg_default_semitransparent">#99121212</color>
<color name="bg_inverse">@color/grey950</color>
<color name="fg_default">#FFFFFF</color>

View File

@ -67,6 +67,7 @@
<color name="fg_inverse">#FFFFFF</color>
<color name="bg_default">#FFFFFF</color>
<color name="bg_default_semitransparent">#99FFFFFF</color>
<color name="bg_inverse">@color/grey950</color>
<color name="bg_dark_mention_chips">#333333</color>

View File

@ -38,6 +38,8 @@
<dimen name="message_bubble_corners_radius">6dp</dimen>
<dimen name="message_bubble_corners_padding">8dp</dimen>
<dimen name="geocoding_result_text_size">18sp</dimen>
<dimen name="maximum_file_preview_size">192dp</dimen>
<dimen name="large_preview_dimension">80dp</dimen>
@ -50,6 +52,7 @@
<dimen name="standard_double_margin">32dp</dimen>
<dimen name="empty_list_icon_layout_width">72dp</dimen>
<dimen name="empty_list_icon_layout_height">72dp</dimen>
<dimen name="standard_double_padding">32dp</dimen>
<dimen name="standard_padding">16dp</dimen>
<dimen name="standard_half_padding">8dp</dimen>
<dimen name="standard_half_margin">8dp</dimen>

View File

@ -54,4 +54,11 @@
<string name="google_app_id" translatable="false">1:829118773643:android:54b65087c544d819</string>
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
<string name="google_storage_bucket" translatable="false">nextcloud-a7dea.appspot.com</string>
<!-- Map and Geocoding -->
<string name="osm_tile_server_url" translatable="false">https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png</string>
<string name="osm_tile_server_attributation" translatable="false">OpenStreetMap contributors</string>
<string name="osm_geocoder_url" translatable="false">https://nominatim.openstreetmap.org/</string>
<string name="osm_geocoder_contact" translatable="false">android@nextcloud.com</string>
</resources>

View File

@ -275,12 +275,14 @@
<string name="nc_sent_an_audio" formatted="true">%1$s sent an audio.</string>
<string name="nc_sent_a_video" formatted="true">%1$s sent a video.</string>
<string name="nc_sent_an_image" formatted="true">%1$s sent an image.</string>
<string name="nc_sent_location" formatted="true">%1$s sent a location.</string>
<string name="nc_sent_a_link_you">You sent a link.</string>
<string name="nc_sent_a_gif_you">You sent a GIF.</string>
<string name="nc_sent_an_attachment_you">You sent an attachment.</string>
<string name="nc_sent_an_audio_you">You sent an audio.</string>
<string name="nc_sent_a_video_you">You sent a video.</string>
<string name="nc_sent_an_image_you">You sent an image.</string>
<string name="nc_sent_location_you">You sent a location.</string>
<string name="nc_formatted_message" translatable="false">%1$s: %2$s</string>
<string name="nc_message_quote_cancel_reply">Cancel reply</string>
<!-- When translating to German, please use non-formal variant -->
@ -371,6 +373,13 @@
<string name="nc_upload_confirm_send_single">Send this file to %1$s?</string>
<string name="nc_upload_in_progess">Uploading</string>
<!-- location sharing -->
<string name="nc_share_location">Share location</string>
<string name="nc_location_permission_required">location permission is required</string>
<string name="nc_share_current_location">Share current location</string>
<string name="nc_share_this_location">Share this location</string>
<string name="nc_location_current_position_description">Your current location</string>
<!-- Phonebook Integration -->
<string name="nc_settings_phone_book_integration_key" translatable="false">phone_book_integration</string>
<string name="nc_settings_phone_book_integration_desc">Match contacts based on phone number to integrate Talk shortcut into system contacts app</string>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
<path d="M12,2C6.514,2 2,6.514 2,12C2,17.486 6.514,22 12,22C17.486,22 22,17.486 22,12C22,6.514 17.486,2 12,2Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.602263,0,0,0.602263,4.77284,4.77284)">
<path d="M12,11.5C10.629,11.5 9.5,10.371 9.5,9C9.5,7.629 10.629,6.5 12,6.5C13.371,6.5 14.5,7.629 14.5,9C14.5,10.371 13.371,11.5 12,11.5M12,2C8.16,2 5,5.16 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9C19,5.16 15.84,2 12,2Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1002 B

View File

@ -1 +1 @@
448
436

View File

@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE
<span class="mdl-layout-title">Lint Report: 3 errors and 290 warnings</span>
<span class="mdl-layout-title">Lint Report: 3 errors and 275 warnings</span>