diff --git a/app/build.gradle b/app/build.gradle
index 2e7549fb1..be6d6623c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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}"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b92c45f31..acca95c7a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -70,6 +70,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt
new file mode 100644
index 000000000..698d319b2
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt
@@ -0,0 +1,58 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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 .
+ */
+
+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) : 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
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
new file mode 100644
index 000000000..8be30ca94
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
@@ -0,0 +1,269 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger
+ * Copyright (C) 2021 Marcel Hibbe
+ * Copyright (C) 2017-2018 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 .
+ */
+
+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(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(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 = 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"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
new file mode 100644
index 000000000..2e181c1cb
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
@@ -0,0 +1,45 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger
+ *
+ * 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 .
+ */
+
+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;
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
index abf27d53a..a7b47438c 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
@@ -2,6 +2,8 @@
* Nextcloud Talk application
*
* @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger
* Copyright (C) 2017-2018 Mario Danic
*
* 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(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)
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
index a573c4956..520e20294 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
@@ -2,6 +2,8 @@
* Nextcloud Talk application
*
* @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger
* Copyright (C) 2017-2018 Mario Danic
*
* 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(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>? = 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)
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
index 6f899c11a..c6ddc7433 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
@@ -3,8 +3,10 @@
*
* @author Mario Danic
* @author Marcel Hibbe
- * Copyright (C) 2017-2018 Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger
* Copyright (C) 2021 Marcel Hibbe
+ * Copyright (C) 2017-2018 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
@@ -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 {
+public abstract class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder {
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);
}
});
-
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
new file mode 100644
index 000000000..6ef0c283b
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
@@ -0,0 +1,250 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2017-2018 Mario Danic
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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 .
+ */
+
+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(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 = 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"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
new file mode 100644
index 000000000..4b676c385
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
@@ -0,0 +1,45 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger
+ *
+ * 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 .
+ */
+
+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;
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java
index 2b16e13dd..85cb7b887 100644
--- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java
+++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java
@@ -406,4 +406,12 @@ public interface NcApi {
@GET
Call downloadResizedImage(@Header("Authorization") String authorization,
@Url String url);
+
+ @FormUrlEncoded
+ @POST
+ Observable sendLocation(@Header("Authorization") String authorization,
+ @Url String url,
+ @Field("objectType") String objectType,
+ @Field("objectId") String objectId,
+ @Field("metaData") String metaData);
}
diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
index 18d560ef2..13975de25 100644
--- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
+++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
@@ -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,
MessagesListAdapter.OnMessageViewLongClickListener,
- MessageHolders.ContentChecker {
+ ContentChecker {
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}"
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt b/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt
new file mode 100644
index 000000000..332c69051
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt
@@ -0,0 +1,220 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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 .
+ */
+
+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 = 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) {
+ 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 = ArrayList()
+ try {
+ results = nominatimClient!!.search(query) as ArrayList
+ 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) {
+ withContext(Main) {
+ initAdapter(results)
+ }
+ }
+
+ interface GeocodingResultListener {
+ fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String)
+ }
+
+ companion object {
+ private const val TAG = "GeocodingController"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt b/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt
new file mode 100644
index 000000000..ade4d75a0
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt
@@ -0,0 +1,492 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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 .
+ */
+
+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 {
+ 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,
+ 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
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/interfaces/ExtendedIMessage.kt b/app/src/main/java/com/nextcloud/talk/interfaces/ExtendedIMessage.kt
new file mode 100644
index 000000000..a494eeded
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/interfaces/ExtendedIMessage.kt
@@ -0,0 +1,28 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
+ *
+ * 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 .
+ */
+
+package com.nextcloud.talk.interfaces
+
+import com.stfalcon.chatkit.commons.models.IMessage
+
+interface ExtendedIMessage : IMessage {
+
+ fun isLocationMessage(): Boolean
+}
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
index aa704cb35..a328fdf53 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
+++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
@@ -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.NONE;
@JsonIgnore
- List 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 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 individualHashMap = messageParameters.get(key);
- if (individualHashMap.get("type").equals("file")) {
+ for (HashMap.Entry> entry : messageParameters.entrySet()) {
+ Map 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> entry : messageParameters.entrySet()) {
+ Map 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 individualHashMap = messageParameters.get(key);
- if (individualHashMap.get("type").equals("file")) {
+ for (HashMap.Entry> entry : messageParameters.entrySet()) {
+ Map 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 {
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt
index 9e060409f..984a858ef 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt
@@ -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())
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
index 29e47b131..89b4b88a7 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
@@ -2,7 +2,7 @@
* Nextcloud Talk application
*
* @author Marcel Hibbe
- * Copyright (C) 2021 Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe
*
* 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()
diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
index ebb129776..218b68069 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
@@ -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";
+ }
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
index 397c084ea..48408670a 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
@@ -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);
diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
index 39ea5d998..bc66d2691 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
@@ -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"
}
diff --git a/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java b/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java
new file mode 100644
index 000000000..380a03244
--- /dev/null
+++ b/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java
@@ -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
+ * .
+ * [/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 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>() {
+ }.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 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>() {
+ }.getType());
+ }
+ }
+
+ return new ArrayList<>();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see fr.dudie.nominatim.client.NominatimClient#search(java.lang.String)
+ */
+ @Override
+ public List 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 lookupAddress(final List typeId) throws IOException {
+
+ final NominatimLookupRequest q = new NominatimLookupRequest();
+ q.setQuery(typeId);
+ return this.lookupAddress(q);
+ }
+}
diff --git a/app/src/main/res/drawable-night/ic_circular_location.xml b/app/src/main/res/drawable-night/ic_circular_location.xml
new file mode 100644
index 000000000..d000c0bf1
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_circular_location.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/current_location_circle.xml b/app/src/main/res/drawable/current_location_circle.xml
new file mode 100644
index 000000000..fc090e473
--- /dev/null
+++ b/app/src/main/res/drawable/current_location_circle.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml b/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml
new file mode 100644
index 000000000..79c4bf857
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_location_on_24.xml b/app/src/main/res/drawable/ic_baseline_location_on_24.xml
new file mode 100644
index 000000000..5c29525a4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_location_on_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml b/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml
new file mode 100644
index 000000000..d8b0f5f0c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_circular_location.xml b/app/src/main/res/drawable/ic_circular_location.xml
new file mode 100644
index 000000000..58a22072a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_circular_location.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/controller_geocoding.xml b/app/src/main/res/layout/controller_geocoding.xml
new file mode 100644
index 000000000..5630edd04
--- /dev/null
+++ b/app/src/main/res/layout/controller_geocoding.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/controller_location.xml b/app/src/main/res/layout/controller_location.xml
new file mode 100644
index 000000000..ed6b775df
--- /dev/null
+++ b/app/src/main/res/layout/controller_location.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_attachment.xml b/app/src/main/res/layout/dialog_attachment.xml
index 70ccc5e38..c2b170f3f 100644
--- a/app/src/main/res/layout/dialog_attachment.xml
+++ b/app/src/main/res/layout/dialog_attachment.xml
@@ -38,6 +38,38 @@
android:textColor="@color/medium_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_custom_incoming_location_message.xml b/app/src/main/res/layout/item_custom_incoming_location_message.xml
new file mode 100644
index 000000000..88f2275b2
--- /dev/null
+++ b/app/src/main/res/layout/item_custom_incoming_location_message.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_custom_incoming_text_message.xml b/app/src/main/res/layout/item_custom_incoming_text_message.xml
index 3085befdc..2c3449847 100644
--- a/app/src/main/res/layout/item_custom_incoming_text_message.xml
+++ b/app/src/main/res/layout/item_custom_incoming_text_message.xml
@@ -2,6 +2,8 @@
~ Nextcloud Talk application
~
~ @author Mario Danic
+ ~ @author Andy Scherzinger
+ ~ Copyright (C) 2021 Andy Scherzinger
~ Copyright (C) 2017-2018 Mario Danic
~
~ This program is free software: you can redistribute it and/or modify
@@ -47,7 +49,10 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_custom_outcoming_text_message.xml b/app/src/main/res/layout/item_custom_outcoming_text_message.xml
index b5ec1fc8c..26c8aed3f 100644
--- a/app/src/main/res/layout/item_custom_outcoming_text_message.xml
+++ b/app/src/main/res/layout/item_custom_outcoming_text_message.xml
@@ -41,7 +41,10 @@
app:flexWrap="wrap"
app:justifyContent="flex_end">
-
+
+
+
+
diff --git a/app/src/main/res/menu/menu_locationpicker.xml b/app/src/main/res/menu/menu_locationpicker.xml
new file mode 100644
index 000000000..b8753d983
--- /dev/null
+++ b/app/src/main/res/menu/menu_locationpicker.xml
@@ -0,0 +1,33 @@
+
+
+
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 12b249d12..f70a55e11 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -39,6 +39,7 @@
#61ffffff
#121212
+ #99121212
@color/grey950
#FFFFFF
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index c1ad8456c..83dad5b08 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -67,6 +67,7 @@
#FFFFFF
#FFFFFF
+ #99FFFFFF
@color/grey950
#333333
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 5cda48c58..8f76e5cc3 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -38,6 +38,8 @@
6dp
8dp
+ 18sp
+
192dp
80dp
@@ -50,6 +52,7 @@
32dp
72dp
72dp
+ 32dp
16dp
8dp
8dp
diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml
index 137ae3611..2435ca1bf 100644
--- a/app/src/main/res/values/setup.xml
+++ b/app/src/main/res/values/setup.xml
@@ -54,4 +54,11 @@
1:829118773643:android:54b65087c544d819
AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s
nextcloud-a7dea.appspot.com
+
+
+ https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
+ OpenStreetMap contributors
+ https://nominatim.openstreetmap.org/
+ android@nextcloud.com
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ef7af6d7e..8fc62aa8a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -275,12 +275,14 @@
%1$s sent an audio.
%1$s sent a video.
%1$s sent an image.
+ %1$s sent a location.
You sent a link.
You sent a GIF.
You sent an attachment.
You sent an audio.
You sent a video.
You sent an image.
+ You sent a location.
%1$s: %2$s
Cancel reply
@@ -371,6 +373,13 @@
Send this file to %1$s?
Uploading
+
+ Share location
+ location permission is required
+ Share current location
+ Share this location
+ Your current location
+
phone_book_integration
Match contacts based on phone number to integrate Talk shortcut into system contacts app
diff --git a/drawable_resources/other/circular_location.svg b/drawable_resources/other/circular_location.svg
new file mode 100644
index 000000000..262957d8d
--- /dev/null
+++ b/drawable_resources/other/circular_location.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt
index 2415c0659..662d98cc9 100644
--- a/scripts/analysis/findbugs-results.txt
+++ b/scripts/analysis/findbugs-results.txt
@@ -1 +1 @@
-448
\ No newline at end of file
+436
\ No newline at end of file
diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt
index b0fd55a3e..96f95f5ca 100644
--- a/scripts/analysis/lint-results.txt
+++ b/scripts/analysis/lint-results.txt
@@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE
- Lint Report: 3 errors and 290 warnings
+ Lint Report: 3 errors and 275 warnings