소스 검색

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

Feature/1136/share location
Andy Scherzinger 4 년 전
부모
커밋
9c18e1c085
47개의 변경된 파일2648개의 추가작업 그리고 207개의 파일을 삭제
  1. 12 1
      app/build.gradle
  2. 3 0
      app/src/main/AndroidManifest.xml
  3. 42 0
      app/src/main/assets/leafletMapMessagePreview.html
  4. 58 0
      app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt
  5. 269 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  6. 45 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
  7. 31 73
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
  8. 26 63
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
  9. 22 26
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  10. 250 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
  11. 45 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
  12. 8 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  13. 40 7
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  14. 220 0
      app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt
  15. 492 0
      app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt
  16. 28 0
      app/src/main/java/com/nextcloud/talk/interfaces/ExtendedIMessage.kt
  17. 63 18
      app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
  18. 2 0
      app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt
  19. 24 1
      app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
  20. 4 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  21. 14 14
      app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
  22. 1 0
      app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
  23. 312 0
      app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java
  24. 34 0
      app/src/main/res/drawable-night/ic_circular_location.xml
  25. 12 0
      app/src/main/res/drawable/current_location_circle.xml
  26. 9 0
      app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml
  27. 10 0
      app/src/main/res/drawable/ic_baseline_location_on_24.xml
  28. 9 0
      app/src/main/res/drawable/ic_baseline_location_on_red_24.xml
  29. 34 0
      app/src/main/res/drawable/ic_circular_location.xml
  30. 34 0
      app/src/main/res/layout/controller_geocoding.xml
  31. 124 0
      app/src/main/res/layout/controller_location.xml
  32. 32 0
      app/src/main/res/layout/dialog_attachment.xml
  33. 56 0
      app/src/main/res/layout/geocoding_item.xml
  34. 91 0
      app/src/main/res/layout/item_custom_incoming_location_message.xml
  35. 6 1
      app/src/main/res/layout/item_custom_incoming_text_message.xml
  36. 83 0
      app/src/main/res/layout/item_custom_outcoming_location_message.xml
  37. 4 1
      app/src/main/res/layout/item_custom_outcoming_text_message.xml
  38. 33 0
      app/src/main/res/menu/menu_geocoding.xml
  39. 33 0
      app/src/main/res/menu/menu_locationpicker.xml
  40. 1 0
      app/src/main/res/values-night/colors.xml
  41. 1 0
      app/src/main/res/values/colors.xml
  42. 3 0
      app/src/main/res/values/dimens.xml
  43. 7 0
      app/src/main/res/values/setup.xml
  44. 9 0
      app/src/main/res/values/strings.xml
  45. 10 0
      drawable_resources/other/circular_location.svg
  46. 1 1
      scripts/analysis/findbugs-results.txt
  47. 1 1
      scripts/analysis/lint-results.txt

+ 12 - 1
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}"

+ 3 - 0
app/src/main/AndroidManifest.xml

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

+ 42 - 0
app/src/main/assets/leafletMapMessagePreview.html

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

+ 58 - 0
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 <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.TextView
+import com.nextcloud.talk.R
+import fr.dudie.nominatim.model.Address
+
+class GeocodingAdapter(context: Context, val dataSource: List<Address>) : BaseAdapter() {
+
+    private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+
+    override fun getCount(): Int {
+        return dataSource.size
+    }
+
+    override fun getItem(position: Int): Any {
+        return dataSource[position]
+    }
+
+    override fun getItemId(position: Int): Long {
+        return position.toLong()
+    }
+
+    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+        val rowView = inflater.inflate(R.layout.geocoding_item, parent, false)
+
+        val nameView = rowView.findViewById(R.id.name) as TextView
+
+        val address = getItem(position) as Address
+        nameView.text = address.displayName
+
+        return rowView
+    }
+}

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

+ 45 - 0
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 <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages;
+
+import android.view.View;
+import android.widget.ProgressBar;
+
+import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
+
+import androidx.emoji.widget.EmojiTextView;
+
+public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder {
+    private final ItemCustomIncomingPreviewMessageBinding binding;
+
+    public IncomingPreviewMessageViewHolder(View itemView) {
+        super(itemView);
+        binding = ItemCustomIncomingPreviewMessageBinding.bind(itemView);
+    }
+
+    public EmojiTextView getMessageText()  {
+        return binding.messageText;
+    }
+
+    public ProgressBar getProgressBar() {
+        return binding.progressBar;
+    }
+}

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

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

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

+ 250 - 0
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 <mario@lovelyhq.com>
+ * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.PorterDuff
+import android.net.Uri
+import android.util.Log
+import android.util.TypedValue
+import android.view.MotionEvent
+import android.view.View
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.Toast
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.view.ViewCompat
+import autodagger.AutoInjector
+import coil.load
+import com.google.android.flexbox.FlexboxLayout
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.models.json.chat.ReadStatus
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.stfalcon.chatkit.messages.MessageHolders
+import java.net.URLEncoder
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
+.OutcomingTextMessageViewHolder<ChatMessage>(incomingView) {
+    private val binding: ItemCustomOutcomingLocationMessageBinding =
+        ItemCustomOutcomingLocationMessageBinding.bind(itemView)
+    private val realView: View = itemView
+
+    var locationLon: String? = ""
+    var locationLat: String? = ""
+    var locationName: String? = ""
+    var locationGeoLink: String? = ""
+
+    @JvmField
+    @Inject
+    var context: Context? = null
+
+    @SuppressLint("SetTextI18n")
+    override fun onBind(message: ChatMessage) {
+        super.onBind(message)
+        sharedApplication!!.componentApplication.inject(this)
+
+        realView.isSelected = false
+        binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60))
+        val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
+        layoutParams.isWrapBefore = false
+
+        val textSize = context!!.resources.getDimension(R.dimen.chat_text_size)
+
+        colorizeMessageBubble(message)
+        binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
+        binding.messageTime.layoutParams = layoutParams
+        binding.messageText.text = message.text
+        binding.messageText.isEnabled = false
+
+        // parent message handling
+        setParentMessageDataOnMessageItem(message)
+
+        val readStatusDrawableInt = when (message.readStatus) {
+            ReadStatus.READ -> R.drawable.ic_check_all
+            ReadStatus.SENT -> R.drawable.ic_check
+            else -> null
+        }
+
+        val readStatusContentDescriptionString = when (message.readStatus) {
+            ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read)
+            ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent)
+            else -> null
+        }
+
+        readStatusDrawableInt?.let { drawableInt ->
+            AppCompatResources.getDrawable(context!!, drawableInt)?.let {
+                it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP)
+                binding.checkMark.setImageDrawable(it)
+            }
+        }
+
+        binding.checkMark.setContentDescription(readStatusContentDescriptionString)
+
+        // geo-location
+        setLocationDataOnMessageItem(message)
+    }
+
+    @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
+    private fun setLocationDataOnMessageItem(message: ChatMessage) {
+        if (message.messageParameters != null && message.messageParameters.size > 0) {
+            for (key in message.messageParameters.keys) {
+                val individualHashMap: Map<String, String> = message.messageParameters[key]!!
+                if (individualHashMap["type"] == "geo-location") {
+                    locationLon = individualHashMap["longitude"]
+                    locationLat = individualHashMap["latitude"]
+                    locationName = individualHashMap["name"]
+                    locationGeoLink = individualHashMap["id"]
+                }
+            }
+        }
+
+        binding.webview.settings?.javaScriptEnabled = true
+
+        binding.webview.webViewClient = object : WebViewClient() {
+            override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
+                return if (url != null && (url.startsWith("http://") || url.startsWith("https://"))
+                ) {
+                    view?.context?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+                    true
+                } else {
+                    false
+                }
+            }
+        }
+
+        val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
+        urlStringBuffer.append(
+            "?mapProviderUrl=" + URLEncoder.encode(context!!.getString(R.string.osm_tile_server_url))
+        )
+        urlStringBuffer.append(
+            "&mapProviderAttribution=" + URLEncoder.encode(
+                context!!.getString(
+                    R.string
+                        .osm_tile_server_attributation
+                )
+            )
+        )
+        urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat))
+        urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon))
+        urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName))
+        urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink))
+
+        binding.webview.loadUrl(urlStringBuffer.toString())
+
+        binding.webview.setOnTouchListener(object : View.OnTouchListener {
+            override fun onTouch(v: View?, event: MotionEvent?): Boolean {
+                when (event?.action) {
+                    MotionEvent.ACTION_UP -> openGeoLink()
+                }
+
+                return v?.onTouchEvent(event) ?: true
+            }
+        })
+    }
+
+    private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
+        if (!message.isDeleted && message.parentMessage != null) {
+            val parentChatMessage = message.parentMessage
+            parentChatMessage.activeUser = message.activeUser
+            parentChatMessage.imageUrl?.let {
+                binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
+                binding.messageQuote.quotedMessageImage.load(it) {
+                    addHeader(
+                        "Authorization",
+                        ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
+                    )
+                }
+            } ?: run {
+                binding.messageQuote.quotedMessageImage.visibility = View.GONE
+            }
+            binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
+                ?: context!!.getText(R.string.nc_nick_guest)
+            binding.messageQuote.quotedMessage.text = parentChatMessage.text
+            binding.messageQuote.quotedMessage.setTextColor(
+                context!!.resources.getColor(R.color.nc_outcoming_text_default)
+            )
+            binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey))
+
+            binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
+
+            binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
+        } else {
+            binding.messageQuote.quotedChatMessageView.visibility = View.GONE
+        }
+    }
+
+    private fun colorizeMessageBubble(message: ChatMessage) {
+        val resources = sharedApplication!!.resources
+        val bgBubbleColor = if (message.isDeleted) {
+            resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted)
+        } else {
+            resources.getColor(R.color.bg_message_list_outcoming_bubble)
+        }
+        if (message.isGrouped) {
+            val bubbleDrawable = DisplayUtils.getMessageSelector(
+                bgBubbleColor,
+                resources.getColor(R.color.transparent),
+                bgBubbleColor,
+                R.drawable.shape_grouped_outcoming_message
+            )
+            ViewCompat.setBackground(bubble, bubbleDrawable)
+        } else {
+            val bubbleDrawable = DisplayUtils.getMessageSelector(
+                bgBubbleColor,
+                resources.getColor(R.color.transparent),
+                bgBubbleColor,
+                R.drawable.shape_outcoming_message
+            )
+            ViewCompat.setBackground(bubble, bubbleDrawable)
+        }
+    }
+
+    private fun openGeoLink() {
+        if (!locationGeoLink.isNullOrEmpty()) {
+            val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!)
+            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(geoLinkWithMarker))
+            browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            context!!.startActivity(browserIntent)
+        } else {
+            Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
+            Log.e(TAG, "locationGeoLink was null or empty")
+        }
+    }
+
+    private fun addMarkerToGeoLink(locationGeoLink: String): String {
+        return locationGeoLink.replace("geo:", "geo:0,0?q=")
+    }
+
+    companion object {
+        private const val TAG = "LocOutMessageView"
+    }
+}

+ 45 - 0
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 <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages;
+
+import android.view.View;
+import android.widget.ProgressBar;
+
+import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
+
+import androidx.emoji.widget.EmojiTextView;
+
+public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder {
+    private final ItemCustomOutcomingPreviewMessageBinding binding;
+
+    public OutcomingPreviewMessageViewHolder(View itemView) {
+        super(itemView);
+        binding = ItemCustomOutcomingPreviewMessageBinding.bind(itemView);
+    }
+
+    public EmojiTextView getMessageText()  {
+        return binding.messageText;
+    }
+
+    public ProgressBar getProgressBar() {
+        return binding.progressBar;
+    }
+}

+ 8 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

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

+ 40 - 7
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<Date>,
     MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
-    MessageHolders.ContentChecker<IMessage> {
+    ContentChecker<ChatMessage> {
     private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
 
     @Inject
@@ -397,17 +401,20 @@ class ChatController(args: Bundle) :
             )
 
             messageHolders.setIncomingImageConfig(
-                MagicPreviewMessageViewHolder::class.java,
+                IncomingPreviewMessageViewHolder::class.java,
                 R.layout.item_custom_incoming_preview_message
             )
+
             messageHolders.setOutcomingImageConfig(
-                MagicPreviewMessageViewHolder::class.java,
+                OutcomingPreviewMessageViewHolder::class.java,
                 R.layout.item_custom_outcoming_preview_message
             )
 
             messageHolders.registerContentType(
-                CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java,
-                R.layout.item_system_message, MagicSystemMessageViewHolder::class.java,
+                CONTENT_TYPE_SYSTEM_MESSAGE,
+                MagicSystemMessageViewHolder::class.java,
+                R.layout.item_system_message,
+                MagicSystemMessageViewHolder::class.java,
                 R.layout.item_system_message,
                 this
             )
@@ -420,6 +427,15 @@ class ChatController(args: Bundle) :
                 R.layout.item_date_header, this
             )
 
+            messageHolders.registerContentType(
+                CONTENT_TYPE_LOCATION,
+                IncomingLocationMessageViewHolder::class.java,
+                R.layout.item_custom_incoming_location_message,
+                OutcomingLocationMessageViewHolder::class.java,
+                R.layout.item_custom_outcoming_location_message,
+                this
+            )
+
             var senderId = ""
             if (!conversationUser?.userId.equals("?")) {
                 senderId = "users/" + conversationUser?.userId
@@ -793,6 +809,18 @@ class ChatController(args: Bundle) :
         )
     }
 
+    fun showShareLocationScreen() {
+        Log.d(TAG, "showShareLocationScreen")
+
+        val bundle = Bundle()
+        bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+        router.pushController(
+            RouterTransaction.with(LocationPickerController(bundle))
+                .pushChangeHandler(HorizontalChangeHandler())
+                .popChangeHandler(HorizontalChangeHandler())
+        )
+    }
+
     private fun showConversationInfoScreen() {
         val bundle = Bundle()
         bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
@@ -1861,6 +1889,8 @@ class ChatController(args: Bundle) :
 
         if (message.hasFileAttachment()) return false
 
+        if (OBJECT_MESSAGE.equals(message.message)) return false
+
         val isOlderThanSixHours = message
             .createdAt
             ?.before(Date(System.currentTimeMillis() - AGE_THREHOLD_FOR_DELETE_MESSAGE)) == true
@@ -1878,8 +1908,9 @@ class ChatController(args: Bundle) :
         return true
     }
 
-    override fun hasContentFor(message: IMessage, type: Byte): Boolean {
+    override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {
         return when (type) {
+            CONTENT_TYPE_LOCATION -> return message.isLocationMessage()
             CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
             CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
             else -> false
@@ -1985,6 +2016,7 @@ class ChatController(args: Bundle) :
         private const val TAG = "ChatController"
         private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
         private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
+        private const val CONTENT_TYPE_LOCATION: Byte = 3
         private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
         private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
         private const val LOBBY_TIMER_DELAY: Long = 5000
@@ -1992,5 +2024,6 @@ class ChatController(args: Bundle) :
         private const val MESSAGE_MAX_LENGTH: Int = 1000
         private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
         private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
+        private const val OBJECT_MESSAGE: String = "{object}"
     }
 }

+ 220 - 0
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 <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.controllers
+
+import android.app.SearchManager
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import android.text.InputType
+import android.util.Log
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.AdapterView
+import android.widget.Toast
+import androidx.appcompat.widget.SearchView
+import androidx.core.view.MenuItemCompat
+import androidx.preference.PreferenceManager
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.adapters.GeocodingAdapter
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.controllers.base.NewBaseController
+import com.nextcloud.talk.controllers.util.viewBinding
+import com.nextcloud.talk.databinding.ControllerGeocodingBinding
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.database.user.UserUtils
+import fr.dudie.nominatim.client.TalkJsonNominatimClient
+import fr.dudie.nominatim.model.Address
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import org.osmdroid.config.Configuration
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class GeocodingController(args: Bundle) :
+    NewBaseController(
+        R.layout.controller_geocoding,
+        args
+    ),
+    SearchView.OnQueryTextListener {
+    private val binding: ControllerGeocodingBinding by viewBinding(ControllerGeocodingBinding::bind)
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    @Inject
+    lateinit var userUtils: UserUtils
+
+    @Inject
+    lateinit var okHttpClient: OkHttpClient
+
+    var roomToken: String?
+    var nominatimClient: TalkJsonNominatimClient? = null
+
+    var searchItem: MenuItem? = null
+    var searchView: SearchView? = null
+    var query: String? = null
+
+    lateinit var adapter: GeocodingAdapter
+    private var geocodingResults: List<Address> = ArrayList()
+
+    constructor(args: Bundle, listener: LocationPickerController) : this(args) {
+        targetController = listener
+    }
+
+    init {
+        setHasOptionsMenu(true)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+        Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
+        query = args.getString(BundleKeys.KEY_GEOCODING_QUERY)
+        roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN)
+    }
+
+    private fun initAdapter(addresses: List<Address>) {
+        adapter = GeocodingAdapter(binding.geocodingResults.context!!, addresses)
+        binding.geocodingResults.adapter = adapter
+    }
+
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+
+        initAdapter(geocodingResults)
+
+        initGeocoder()
+        if (!query.isNullOrEmpty()) {
+            searchLocation()
+        } else {
+            Log.e(TAG, "search string that was passed to GeocodingController was null or empty")
+        }
+
+        binding.geocodingResults.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
+            val address: Address = adapter.getItem(position) as Address
+            val listener: GeocodingResultListener? = targetController as GeocodingResultListener?
+            listener?.receiveChosenGeocodingResult(address.latitude, address.longitude, address.displayName)
+            router.popCurrentController()
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        super.onCreateOptionsMenu(menu, inflater)
+        inflater.inflate(R.menu.menu_geocoding, menu)
+        searchItem = menu.findItem(R.id.geocoding_action_search)
+        initSearchView()
+
+        searchItem?.expandActionView()
+        searchView?.setQuery(query, false)
+        searchView?.clearFocus()
+    }
+
+    override fun onQueryTextSubmit(query: String?): Boolean {
+        this.query = query
+        searchLocation()
+        searchView?.clearFocus()
+        return true
+    }
+
+    override fun onQueryTextChange(newText: String?): Boolean {
+        return true
+    }
+
+    private fun initSearchView() {
+        if (activity != null) {
+            val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
+            if (searchItem != null) {
+                searchView = MenuItemCompat.getActionView(searchItem) as SearchView
+                searchView?.maxWidth = Int.MAX_VALUE
+                searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
+                var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
+                    imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+                }
+                searchView?.imeOptions = imeOptions
+                searchView?.queryHint = resources!!.getString(R.string.nc_search)
+                searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
+                searchView?.setOnQueryTextListener(this)
+
+                searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
+                    override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
+                        return true
+                    }
+
+                    override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
+                        router.popCurrentController()
+                        return true
+                    }
+                })
+            }
+        }
+    }
+
+    private fun initGeocoder() {
+        val baseUrl = context!!.getString(R.string.osm_geocoder_url)
+        val email = context!!.getString(R.string.osm_geocoder_contact)
+        nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email)
+    }
+
+    private fun searchLocation(): Boolean {
+        CoroutineScope(IO).launch {
+            executeGeocodingRequest()
+        }
+        return true
+    }
+
+    @Suppress("Detekt.TooGenericExceptionCaught")
+    private suspend fun executeGeocodingRequest() {
+        var results: ArrayList<Address> = ArrayList()
+        try {
+            results = nominatimClient!!.search(query) as ArrayList<Address>
+            for (address in results) {
+                Log.d(TAG, address.displayName)
+                Log.d(TAG, address.latitude.toString())
+                Log.d(TAG, address.longitude.toString())
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to get geocoded addresses", e)
+            Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
+        }
+        updateResultsOnMainThread(results)
+    }
+
+    private suspend fun updateResultsOnMainThread(results: ArrayList<Address>) {
+        withContext(Main) {
+            initAdapter(results)
+        }
+    }
+
+    interface GeocodingResultListener {
+        fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String)
+    }
+
+    companion object {
+        private const val TAG = "GeocodingController"
+    }
+}

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

+ 28 - 0
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 <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.interfaces
+
+import com.stfalcon.chatkit.commons.models.IMessage
+
+interface ExtendedIMessage : IMessage {
+
+    fun isLocationMessage(): Boolean
+}

+ 63 - 18
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 = ReadStatus.NONE;
 
     @JsonIgnore
-    List<MessageType> messageTypesToIgnore = Arrays.asList(MessageType.REGULAR_TEXT_MESSAGE,
-            MessageType.SYSTEM_MESSAGE, MessageType.SINGLE_LINK_VIDEO_MESSAGE,
-            MessageType.SINGLE_LINK_AUDIO_MESSAGE, MessageType.SINGLE_LINK_MESSAGE);
+    List<MessageType> messageTypesToIgnore = Arrays.asList(
+            MessageType.REGULAR_TEXT_MESSAGE,
+            MessageType.SYSTEM_MESSAGE,
+            MessageType.SINGLE_LINK_VIDEO_MESSAGE,
+            MessageType.SINGLE_LINK_AUDIO_MESSAGE,
+            MessageType.SINGLE_LINK_MESSAGE,
+            MessageType.SINGLE_NC_GEOLOCATION_MESSAGE);
 
     public boolean hasFileAttachment() {
         if (messageParameters != null && messageParameters.size() > 0) {
-            for (String key : messageParameters.keySet()) {
-                Map<String, String> individualHashMap = messageParameters.get(key);
-                if (individualHashMap.get("type").equals("file")) {
+            for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
+                Map<String, String> individualHashMap = entry.getValue();
+                if(MessageDigest.isEqual(
+                        Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
+                        ("file").getBytes(Charsets.UTF_8))) {
                     return true;
                 }
             }
@@ -103,13 +112,31 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
         return false;
     }
 
+    private boolean hasGeoLocation() {
+        if (messageParameters != null && messageParameters.size() > 0) {
+            for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
+                Map<String, String> individualHashMap = entry.getValue();
+
+                if(MessageDigest.isEqual(
+                        Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
+                        ("geo-location").getBytes(Charsets.UTF_8))) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
     @Nullable
     @Override
     public String getImageUrl() {
         if (messageParameters != null && messageParameters.size() > 0) {
-            for (String key : messageParameters.keySet()) {
-                Map<String, String> individualHashMap = messageParameters.get(key);
-                if (individualHashMap.get("type").equals("file")) {
+            for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
+                Map<String, String> individualHashMap = entry.getValue();
+                if(MessageDigest.isEqual(
+                        Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
+                        ("file").getBytes(Charsets.UTF_8))) {
                     selectedIndividualHashMap = individualHashMap;
                     return (ApiUtils.getUrlForFilePreviewWithFileId(getActiveUser().getBaseUrl(),
                             individualHashMap.get("id"), NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size)));
@@ -133,6 +160,11 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
             return MessageType.SINGLE_NC_ATTACHMENT_MESSAGE;
         }
 
+        if (hasGeoLocation()) {
+            return MessageType.SINGLE_NC_GEOLOCATION_MESSAGE;
+        }
+
+
         return TextMatchers.getMessageTypeFromString(getText());
     }
 
@@ -158,22 +190,29 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
         if (getMessageType().equals(MessageType.REGULAR_TEXT_MESSAGE) || getMessageType().equals(MessageType.SYSTEM_MESSAGE) || getMessageType().equals(MessageType.SINGLE_LINK_MESSAGE)) {
             return getText();
         } else {
-            if (getMessageType().equals(MessageType.SINGLE_LINK_GIPHY_MESSAGE)
-                    || getMessageType().equals(MessageType.SINGLE_LINK_TENOR_MESSAGE)
-                    || getMessageType().equals(MessageType.SINGLE_LINK_GIF_MESSAGE)) {
+            if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getMessageType()
+                    || MessageType.SINGLE_LINK_TENOR_MESSAGE == getMessageType()
+                    || MessageType.SINGLE_LINK_GIF_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_gif_you));
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_gif),
                             !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
-            } else if (getMessageType().equals(MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)) {
+            } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_attachment_you));
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_attachment),
                             !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
+            } else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getMessageType()) {
+                if (getActorId().equals(getActiveUser().getUserId())) {
+                    return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_location_you));
+                } else {
+                    return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_location),
+                                          !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
+                }
             /*} else if (getMessageType().equals(MessageType.SINGLE_LINK_MESSAGE)) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_link_you));
@@ -181,21 +220,21 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_link),
                             !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }*/
-            } else if (getMessageType().equals(MessageType.SINGLE_LINK_AUDIO_MESSAGE)) {
+            } else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_audio_you));
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_audio),
                             !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
-            } else if (getMessageType().equals(MessageType.SINGLE_LINK_VIDEO_MESSAGE)) {
+            } else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_video_you));
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_video),
                             !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
-            } else if (getMessageType().equals(MessageType.SINGLE_LINK_IMAGE_MESSAGE)) {
+            } else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_image_you));
                 } else {
@@ -537,6 +576,11 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
         return "ChatMessage(isGrouped=" + this.isGrouped() + ", isOneToOneConversation=" + this.isOneToOneConversation() + ", activeUser=" + this.getActiveUser() + ", selectedIndividualHashMap=" + this.getSelectedIndividualHashMap() + ", isLinkPreviewAllowed=" + this.isLinkPreviewAllowed() + ", isDeleted=" + this.isDeleted() + ", jsonMessageId=" + this.getJsonMessageId() + ", token=" + this.getToken() + ", actorType=" + this.getActorType() + ", actorId=" + this.getActorId() + ", actorDisplayName=" + this.getActorDisplayName() + ", timestamp=" + this.getTimestamp() + ", message=" + this.getMessage() + ", messageParameters=" + this.getMessageParameters() + ", systemMessageType=" + this.getSystemMessageType() + ", replyable=" + this.isReplyable() + ", parentMessage=" + this.getParentMessage() + ", readStatus=" + this.getReadStatus() + ", messageTypesToIgnore=" + this.getMessageTypesToIgnore() + ")";
     }
 
+    @Override
+    public boolean isLocationMessage() {
+        return hasGeoLocation();
+    }
+
     public enum MessageType {
         REGULAR_TEXT_MESSAGE,
         SYSTEM_MESSAGE,
@@ -548,6 +592,7 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
         SINGLE_LINK_IMAGE_MESSAGE,
         SINGLE_LINK_AUDIO_MESSAGE,
         SINGLE_NC_ATTACHMENT_MESSAGE,
+        SINGLE_NC_GEOLOCATION_MESSAGE,
     }
 
     public enum SystemMessageType {

+ 2 - 0
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())

+ 24 - 1
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 <marcel.hibbe@nextcloud.com>
+ * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,7 +22,9 @@ package com.nextcloud.talk.ui.dialog
 
 import android.app.Activity
 import android.os.Bundle
+import android.view.View
 import android.view.ViewGroup
+import android.widget.LinearLayout
 import androidx.appcompat.widget.AppCompatTextView
 import butterknife.BindView
 import butterknife.ButterKnife
@@ -35,6 +37,14 @@ import com.nextcloud.talk.models.database.CapabilitiesUtil
 
 class AttachmentDialog(val activity: Activity, var chatController: ChatController) : BottomSheetDialog(activity) {
 
+    @BindView(R.id.menu_share_location)
+    @JvmField
+    var shareLocationItem: LinearLayout? = null
+
+    @BindView(R.id.txt_share_location)
+    @JvmField
+    var shareLocation: AppCompatTextView? = null
+
     @BindView(R.id.txt_attach_file_from_local)
     @JvmField
     var attachFromLocal: AppCompatTextView? = null
@@ -60,6 +70,19 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
             String.format(it.getString(R.string.nc_upload_from_cloud), serverName)
         }
 
+        if (!CapabilitiesUtil.hasSpreedFeatureCapability(
+                chatController.conversationUser,
+                "geo-location-sharing"
+            )
+        ) {
+            shareLocationItem?.visibility = View.GONE
+        }
+
+        shareLocation?.setOnClickListener {
+            chatController.showShareLocationScreen()
+            dismiss()
+        }
+
         attachFromLocal?.setOnClickListener {
             chatController.sendSelectLocalFileIntent()
             dismiss()

+ 4 - 0
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";
+    }
 }

+ 14 - 14
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);

+ 1 - 0
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"
 }

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

+ 34 - 0
app/src/main/res/drawable-night/ic_circular_location.xml

@@ -0,0 +1,34 @@
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#2C2C2C"
+      android:fillType="nonZero"
+      android:pathData="M12,0C5.4168,0 0,5.4168 0,12C0,18.5832 5.4168,24 12,24C18.5832,24 24,18.5832 24,12C24,5.4168 18.5832,0 12,0Z" />
+  <path
+      android:fillColor="#ffffff"
+      android:fillType="nonZero"
+      android:pathData="M12,11.6989C11.1743,11.6989 10.4943,11.0189 10.4943,10.1932C10.4943,9.3675 11.1743,8.6875 12,8.6875C12.8257,8.6875 13.5057,9.3675 13.5057,10.1932C13.5057,11.0189 12.8257,11.6989 12,11.6989M12,5.9774C9.6873,5.9774 7.7842,7.8805 7.7842,10.1932C7.7842,13.3551 12,18.0226 12,18.0226C12,18.0226 16.2158,13.3551 16.2158,10.1932C16.2158,7.8805 14.3127,5.9774 12,5.9774Z" />
+</vector>

+ 12 - 0
app/src/main/res/drawable/current_location_circle.xml

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

+ 9 - 0
app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml

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

+ 10 - 0
app/src/main/res/drawable/ic_baseline_location_on_24.xml

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

+ 9 - 0
app/src/main/res/drawable/ic_baseline_location_on_red_24.xml

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

+ 34 - 0
app/src/main/res/drawable/ic_circular_location.xml

@@ -0,0 +1,34 @@
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#DBDBDB"
+      android:fillType="nonZero"
+      android:pathData="M12,0C5.4168,0 0,5.4168 0,12C0,18.5832 5.4168,24 12,24C18.5832,24 24,18.5832 24,12C24,5.4168 18.5832,0 12,0Z" />
+  <path
+      android:fillColor="#ffffff"
+      android:fillType="nonZero"
+      android:pathData="M12,11.6989C11.1743,11.6989 10.4943,11.0189 10.4943,10.1932C10.4943,9.3675 11.1743,8.6875 12,8.6875C12.8257,8.6875 13.5057,9.3675 13.5057,10.1932C13.5057,11.0189 12.8257,11.6989 12,11.6989M12,5.9774C9.6873,5.9774 7.7842,7.8805 7.7842,10.1932C7.7842,13.3551 12,18.0226 12,18.0226C12,18.0226 16.2158,13.3551 16.2158,10.1932C16.2158,7.8805 14.3127,5.9774 12,5.9774Z" />
+</vector>

+ 34 - 0
app/src/main/res/layout/controller_geocoding.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/parent_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <ListView
+        android:id="@+id/geocoding_results"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    </ListView>
+
+</LinearLayout>

+ 124 - 0
app/src/main/res/layout/controller_location.xml

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/parent_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+
+        <org.osmdroid.views.MapView
+            android:id="@+id/map"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/centerMapButton"
+            style="@style/Widget.AppTheme.Button.IconButton"
+            android:layout_width="50dp"
+            android:layout_height="50dp"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentEnd="true"
+            android:layout_margin="8dp"
+            android:insetLeft="0dp"
+            android:insetTop="0dp"
+            android:insetRight="0dp"
+            android:insetBottom="0dp"
+            android:padding="0dp"
+            app:backgroundTint="@color/bg_default_semitransparent"
+            app:cornerRadius="@dimen/button_corner_radius"
+            app:elevation="0dp"
+            app:icon="@drawable/ic_baseline_gps_fixed_24"
+            app:iconGravity="textStart"
+            app:iconPadding="0dp"
+            app:iconSize="24dp"
+            app:iconTint="@color/high_emphasis_text" />
+
+        <View
+            android:id="@+id/locationpicker_anchor"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_centerInParent="true" />
+
+        <ImageView
+            android:layout_width="30dp"
+            android:layout_height="50dp"
+            android:layout_above="@id/locationpicker_anchor"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="-10dp"
+            android:contentDescription="@string/nc_location_current_position_description"
+            android:src="@drawable/ic_baseline_location_on_red_24" />
+    </RelativeLayout>
+
+    <LinearLayout
+        android:id="@+id/share_location"
+        android:layout_width="match_parent"
+        android:layout_height="72dp"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:id="@+id/roundedImageView"
+            android:layout_width="@dimen/avatar_size"
+            android:layout_height="@dimen/avatar_size"
+            android:layout_gravity="top"
+            android:layout_margin="@dimen/standard_margin"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_circular_location" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/share_location_description"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="16sp"
+                tools:text="Share this location" />
+
+            <TextView
+                android:id="@+id/place_name"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:ellipsize="end"
+                android:maxLines="1"
+                android:textColor="@color/medium_emphasis_text"
+                android:textSize="14sp"
+                tools:text="Brandenburg, Germany" />
+        </LinearLayout>
+
+    </LinearLayout>
+
+</LinearLayout>

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

+ 56 - 0
app/src/main/res/layout/geocoding_item.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="56dp"
+    android:orientation="horizontal"
+    tools:ignore="UseCompoundDrawables">
+
+    <ImageView
+        android:id="@+id/roundedImageView"
+        android:layout_width="@dimen/avatar_size"
+        android:layout_height="@dimen/avatar_size"
+        android:layout_gravity="top"
+        android:layout_marginTop="@dimen/standard_margin"
+        android:layout_marginStart="@dimen/standard_half_margin"
+        android:layout_marginEnd="@dimen/standard_double_padding"
+        android:layout_marginBottom="@dimen/standard_margin"
+        android:contentDescription="@null"
+        android:src="@drawable/ic_circular_location" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top"
+        android:textColor="@color/high_emphasis_text"
+        android:textSize="@dimen/geocoding_result_text_size"
+        android:paddingTop="12dp"
+        android:paddingBottom="@dimen/standard_padding"
+        android:paddingStart="@dimen/standard_half_padding"
+        android:paddingEnd="@dimen/standard_padding"
+        tools:text="S Sonnenallee, 50, Saalestraße, Rixdorf, Neukölln, Berlin, 12055, Deutschland" />
+
+</LinearLayout>

+ 91 - 0
app/src/main/res/layout/item_custom_incoming_location_message.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ @author Marcel Hibbe
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+        <com.facebook.drawee.view.SimpleDraweeView
+            android:id="@id/messageUserAvatar"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_alignParentTop="true"
+            android:layout_marginEnd="8dp"
+            app:roundAsCircle="true" />
+
+        <com.google.android.flexbox.FlexboxLayout
+            android:id="@id/bubble"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_marginEnd="@dimen/message_incoming_bubble_margin_right"
+            android:layout_toEndOf="@id/messageUserAvatar"
+            android:orientation="vertical"
+            app:alignContent="stretch"
+            app:alignItems="stretch"
+            app:flexWrap="wrap"
+            app:justifyContent="flex_end">
+
+                <include
+                    android:id="@+id/message_quote"
+                    layout="@layout/item_message_quote"
+                    android:visibility="gone" />
+
+                <WebView
+                    android:id="@+id/webview"
+                    android:layout_width="400dp"
+                    android:layout_height="200dp" />
+
+                <androidx.emoji.widget.EmojiTextView
+                    android:id="@+id/messageAuthor"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="4dp"
+                    android:textColor="@color/textColorMaxContrast"
+                    android:textSize="12sp" />
+
+                <androidx.emoji.widget.EmojiTextView
+                    android:id="@id/messageText"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:lineSpacingMultiplier="1.2"
+                    android:textIsSelectable="true"
+                    app:layout_alignSelf="flex_start"
+                    app:layout_flexGrow="1"
+                    app:layout_wrapBefore="true" />
+
+                <TextView
+                    android:id="@id/messageTime"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_below="@id/messageText"
+                    android:layout_marginStart="8dp"
+                    app:layout_alignSelf="center" />
+
+        </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

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

+ 83 - 0
app/src/main/res/layout/item_custom_outcoming_location_message.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:id="@id/bubble"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentEnd="true"
+        android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
+        app:alignContent="stretch"
+        app:alignItems="stretch"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end">
+
+        <include
+            android:id="@+id/message_quote"
+            layout="@layout/item_message_quote"
+            android:visibility="gone" />
+
+        <WebView
+            android:id="@+id/webview"
+            android:layout_width="400dp"
+            android:layout_height="200dp" />
+
+        <androidx.emoji.widget.EmojiTextView
+            android:id="@id/messageText"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignWithParentIfMissing="true"
+            android:lineSpacingMultiplier="1.2"
+            android:textColorHighlight="@color/nc_grey"
+            android:textIsSelectable="true"
+            tools:text="Talk to ayou later!" />
+
+        <TextView
+            android:id="@id/messageTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            app:layout_alignSelf="center"
+            tools:text="10:35" />
+
+        <ImageView
+            android:id="@+id/checkMark"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageTime"
+            android:layout_marginStart="8dp"
+            app:layout_alignSelf="center"
+            android:contentDescription="@null" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

+ 4 - 1
app/src/main/res/layout/item_custom_outcoming_text_message.xml

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

+ 33 - 0
app/src/main/res/menu/menu_geocoding.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017 Mario Danic
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+	<!-- Search, should appear as action button -->
+	<item android:id="@+id/geocoding_action_search"
+	      android:title="@string/nc_search"
+	      android:icon="@drawable/ic_search_white_24dp"
+	      app:showAsAction="collapseActionView|always"
+	      android:animateLayoutChanges="true"
+	      app:actionViewClass="androidx.appcompat.widget.SearchView" />
+
+</menu>

+ 33 - 0
app/src/main/res/menu/menu_locationpicker.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017 Mario Danic
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+	<!-- Search, should appear as action button -->
+	<item android:id="@+id/location_action_search"
+	      android:title="@string/nc_search"
+	      android:icon="@drawable/ic_search_white_24dp"
+	      app:showAsAction="collapseActionView|always"
+	      android:animateLayoutChanges="true"
+	      app:actionViewClass="androidx.appcompat.widget.SearchView" />
+
+</menu>

+ 1 - 0
app/src/main/res/values-night/colors.xml

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

+ 1 - 0
app/src/main/res/values/colors.xml

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

+ 3 - 0
app/src/main/res/values/dimens.xml

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

+ 7 - 0
app/src/main/res/values/setup.xml

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

+ 9 - 0
app/src/main/res/values/strings.xml

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

+ 10 - 0
drawable_resources/other/circular_location.svg

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

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-448
+436

+ 1 - 1
scripts/analysis/lint-results.txt

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