extraMap = new HashMap<>(4);
-        extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
-        extraMap
-            .put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
-        extraMap
-            .put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
-        extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
-        return extraMap;
-    }
-
-    protected void fetchWithRequest(
-        final OkHttpNetworkFetchState fetchState,
-        final NetworkFetcher.Callback callback,
-        final Request request) {
-        final Call call = mCallFactory.newCall(request);
-
-        fetchState
-            .getContext()
-            .addCallbacks(
-                new BaseProducerContextCallbacks() {
-                    @Override
-                    public void onCancellationRequested() {
-                        onFetchCancellationRequested(call);
-                    }
-                });
-
-        call.enqueue(
-            new okhttp3.Callback() {
-                @Override
-                public void onResponse(final Call call, final Response response) {
-                    onFetchResponse(fetchState, call, response, callback);
-                }
-
-                @Override
-                public void onFailure(final Call call, final IOException e) {
-                    handleException(call, e, callback);
-                }
-            });
-    }
-
-    private void onFetchCancellationRequested(final Call call) {
-        if (Looper.myLooper() != Looper.getMainLooper()) {
-            call.cancel();
-        } else {
-            mCancellationExecutor.execute(call::cancel);
-        }
-    }
-
-    private void onFetchResponse(final OkHttpNetworkFetchState fetchState, final Call call,
-        final Response response,
-        final NetworkFetcher.Callback callback) {
-        fetchState.responseTime = SystemClock.elapsedRealtime();
-        try (final ResponseBody body = response.body()) {
-            if (!response.isSuccessful()) {
-                handleException(
-                    call, new IOException("Unexpected HTTP code " + response),
-                    callback);
-                return;
-            }
-
-            final BytesRange responseRange =
-                BytesRange.fromContentRangeHeader(response.header("Content-Range"));
-            if (responseRange != null
-                && !(responseRange.from == 0
-                && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
-                // Only treat as a partial image if the range is not all of the content
-                fetchState.setResponseBytesRange(responseRange);
-                fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
-            }
-
-            long contentLength = body.contentLength();
-            if (contentLength < 0) {
-                contentLength = 0;
-            }
-            callback.onResponse(body.byteStream(), (int) contentLength);
-        } catch (final Exception e) {
-            handleException(call, e, callback);
-        }
-    }
-
-    /**
-     * Handles exceptions.
-     *
-     * OkHttp notifies callers of cancellations via an IOException. If IOException is caught
-     * after request cancellation, then the exception is interpreted as successful cancellation and
-     * onCancellation is called. Otherwise onFailure is called.
-     */
-    private void handleException(final Call call, final Exception e, final Callback callback) {
-        if (call.isCanceled()) {
-            callback.onCancellation();
-        } else {
-            callback.onFailure(e);
-        }
-    }
-
-    public static class OkHttpNetworkFetchState extends FetchState {
-
-        public long submitTime;
-        public long responseTime;
-        public long fetchCompleteTime;
-
-        public OkHttpNetworkFetchState(
-            final Consumer consumer, final ProducerContext producerContext) {
-            super(consumer, producerContext);
-        }
-    }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt
new file mode 100644
index 000000000..c8de4022b
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt
@@ -0,0 +1,199 @@
+package fr.free.nrw.commons.media
+
+import android.os.Looper
+import android.os.SystemClock
+import com.facebook.imagepipeline.common.BytesRange
+import com.facebook.imagepipeline.image.EncodedImage
+import com.facebook.imagepipeline.producers.BaseNetworkFetcher
+import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks
+import com.facebook.imagepipeline.producers.Consumer
+import com.facebook.imagepipeline.producers.FetchState
+import com.facebook.imagepipeline.producers.NetworkFetcher
+import com.facebook.imagepipeline.producers.ProducerContext
+import fr.free.nrw.commons.CommonsApplication
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import okhttp3.CacheControl
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import timber.log.Timber
+import java.io.IOException
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Singleton
+
+// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled
+// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java
+@Singleton
+class CustomOkHttpNetworkFetcher
+@JvmOverloads constructor(
+    private val mCallFactory: Call.Factory,
+    private val mCancellationExecutor: Executor,
+    private val defaultKvStore: JsonKvStore,
+    disableOkHttpCache: Boolean = true
+) : BaseNetworkFetcher() {
+
+    private val mCacheControl =
+        if (disableOkHttpCache) CacheControl.Builder().noStore().build() else null
+    private val isLimitedConnectionMode: Boolean
+        get() = defaultKvStore.getBoolean(
+            CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
+            false
+        )
+
+    /**
+     * @param okHttpClient client to use
+     */
+    @Inject
+    constructor(
+        okHttpClient: OkHttpClient,
+        @Named("default_preferences") defaultKvStore: JsonKvStore
+    ) : this(okHttpClient, okHttpClient.dispatcher.executorService, defaultKvStore)
+
+    /**
+     * @param mCallFactory          custom [Call.Factory] for fetching image from the network
+     * @param mCancellationExecutor executor on which fetching cancellation is performed if
+     * cancellation is requested from the UI Thread
+     * @param disableOkHttpCache   true if network requests should not be cached by OkHttp
+     */
+    override fun createFetchState(consumer: Consumer, context: ProducerContext) =
+        OkHttpNetworkFetchState(consumer, context)
+
+    override fun fetch(
+        fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback
+    ) {
+        fetchState.submitTime = SystemClock.elapsedRealtime()
+
+        try {
+            if (isLimitedConnectionMode) {
+                Timber.d("Skipping loading of image as limited connection mode is enabled")
+                callback.onFailure(Exception("Failing image request as limited connection mode is enabled"))
+                return
+            }
+
+            val requestBuilder = Request.Builder().url(fetchState.uri.toString()).get()
+
+            if (mCacheControl != null) {
+                requestBuilder.cacheControl(mCacheControl)
+            }
+
+            val bytesRange = fetchState.context.imageRequest.bytesRange
+            if (bytesRange != null) {
+                requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue())
+            }
+
+            fetchWithRequest(fetchState, callback, requestBuilder.build())
+        } catch (e: Exception) {
+            // handle error while creating the request
+            callback.onFailure(e)
+        }
+    }
+
+    override fun onFetchCompletion(fetchState: OkHttpNetworkFetchState, byteSize: Int) {
+        fetchState.fetchCompleteTime = SystemClock.elapsedRealtime()
+    }
+
+    override fun getExtraMap(fetchState: OkHttpNetworkFetchState, byteSize: Int) =
+        fetchState.toExtraMap(byteSize)
+
+    private fun fetchWithRequest(
+        fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback, request: Request
+    ) {
+        val call = mCallFactory.newCall(request)
+
+        fetchState.context.addCallbacks(object : BaseProducerContextCallbacks() {
+            override fun onCancellationRequested() {
+                onFetchCancellationRequested(call)
+            }
+        })
+
+        call.enqueue(object : Callback {
+            override fun onResponse(call: Call, response: Response) =
+                onFetchResponse(fetchState, call, response, callback)
+
+            override fun onFailure(call: Call, e: IOException) =
+                handleException(call, e, callback)
+        })
+    }
+
+    private fun onFetchCancellationRequested(call: Call) {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            call.cancel()
+        } else {
+            mCancellationExecutor.execute { call.cancel() }
+        }
+    }
+
+    private fun onFetchResponse(
+        fetchState: OkHttpNetworkFetchState,
+        call: Call,
+        response: Response,
+        callback: NetworkFetcher.Callback
+    ) {
+        fetchState.responseTime = SystemClock.elapsedRealtime()
+        try {
+            response.body.use { body ->
+                if (!response.isSuccessful) {
+                    handleException(call, IOException("Unexpected HTTP code $response"), callback)
+                    return
+                }
+                val responseRange =
+                    BytesRange.fromContentRangeHeader(response.header("Content-Range"))
+                if (responseRange != null && !(responseRange.from == 0 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
+                    // Only treat as a partial image if the range is not all of the content
+                    fetchState.responseBytesRange = responseRange
+                    fetchState.onNewResultStatusFlags = Consumer.IS_PARTIAL_RESULT
+                }
+
+                var contentLength = body!!.contentLength()
+                if (contentLength < 0) {
+                    contentLength = 0
+                }
+                callback.onResponse(body.byteStream(), contentLength.toInt())
+            }
+        } catch (e: Exception) {
+            handleException(call, e, callback)
+        }
+    }
+
+    /**
+     * Handles exceptions.
+     *
+     * OkHttp notifies callers of cancellations via an IOException. If IOException is caught
+     * after request cancellation, then the exception is interpreted as successful cancellation and
+     * onCancellation is called. Otherwise onFailure is called.
+     */
+    private fun handleException(call: Call, e: Exception, callback: NetworkFetcher.Callback) {
+        if (call.isCanceled()) {
+            callback.onCancellation()
+        } else {
+            callback.onFailure(e)
+        }
+    }
+}
+
+class OkHttpNetworkFetchState(
+    consumer: Consumer?, producerContext: ProducerContext?
+) : FetchState(consumer, producerContext) {
+    var submitTime: Long = 0
+    var responseTime: Long = 0
+    var fetchCompleteTime: Long = 0
+
+    fun toExtraMap(byteSize: Int) = buildMap {
+        put(QUEUE_TIME, (responseTime - submitTime).toString())
+        put(FETCH_TIME, (fetchCompleteTime - responseTime).toString())
+        put(TOTAL_TIME, (fetchCompleteTime - submitTime).toString())
+        put(IMAGE_SIZE, byteSize.toString())
+    }
+
+    companion object {
+        private const val QUEUE_TIME = "queue_time"
+        private const val FETCH_TIME = "fetch_time"
+        private const val TOTAL_TIME = "total_time"
+        private const val IMAGE_SIZE = "image_size"
+    }
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt
deleted file mode 100644
index fe96eb8cb..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package fr.free.nrw.commons.media
-
-data class IdAndCaptions(
-    val id: String,
-    val captions: Map,
-)
diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
new file mode 100644
index 000000000..c989ee7e3
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
@@ -0,0 +1,18 @@
+package fr.free.nrw.commons.media
+
+data class IdAndLabels(
+    val id: String,
+    val labels: Map,
+) {
+    // if a label is available in user's locale, return it
+    // if not then check for english, else show any available.
+    fun getLocalizedLabel(locale: String): String? {
+        if (labels[locale] != null) {
+            return labels[locale]
+        }
+        if (labels["en"] != null) {
+            return labels["en"]
+        }
+        return labels.values.firstOrNull() ?: id
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt
new file mode 100644
index 000000000..ccc176154
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt
@@ -0,0 +1,76 @@
+package fr.free.nrw.commons.media
+
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentStatePagerAdapter
+import fr.free.nrw.commons.media.MediaDetailFragment.Companion.forMedia
+import timber.log.Timber
+
+// FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
+class MediaDetailAdapter(
+    val mediaDetailPagerFragment: MediaDetailPagerFragment,
+    fm: FragmentManager
+) : FragmentStatePagerAdapter(fm) {
+    /**
+     * Keeps track of the current displayed fragment.
+     */
+    private var currentFragment: Fragment? = null
+
+    override fun getItem(i: Int): Fragment {
+        if (i == 0) {
+            // See bug https://code.google.com/p/android/issues/detail?id=27526
+            if (mediaDetailPagerFragment.activity == null) {
+                Timber.d("Skipping getItem. Returning as activity is destroyed!")
+                return Fragment()
+            }
+            mediaDetailPagerFragment.binding!!.mediaDetailsPager.postDelayed(
+                { mediaDetailPagerFragment.requireActivity().invalidateOptionsMenu() }, 5
+            )
+        }
+        return if (mediaDetailPagerFragment.isFromFeaturedRootFragment) {
+            forMedia(
+                mediaDetailPagerFragment.position + i,
+                mediaDetailPagerFragment.editable, mediaDetailPagerFragment.isFeaturedImage,
+                mediaDetailPagerFragment.isWikipediaButtonDisplayed
+            )
+        } else {
+            forMedia(
+                i, mediaDetailPagerFragment.editable,
+                mediaDetailPagerFragment.isFeaturedImage,
+                mediaDetailPagerFragment.isWikipediaButtonDisplayed
+            )
+        }
+    }
+
+    override fun getCount(): Int {
+        if (mediaDetailPagerFragment.activity == null) {
+            Timber.d("Skipping getCount. Returning as activity is destroyed!")
+            return 0
+        }
+        return mediaDetailPagerFragment.mediaDetailProvider!!.getTotalMediaCount()
+    }
+
+    /**
+     * If current fragment is of type MediaDetailFragment, return it, otherwise return null.
+     *
+     * @return MediaDetailFragment
+     */
+    val currentMediaDetailFragment: MediaDetailFragment?
+        get() = currentFragment as? MediaDetailFragment
+
+    /**
+     * Called to inform the adapter of which item is currently considered to be the "primary", that
+     * is the one show to the user as the current page.
+     */
+    override fun setPrimaryItem(
+        container: ViewGroup, position: Int,
+        obj: Any
+    ) {
+        // Update the current fragment if changed
+        if (currentFragment !== obj) {
+            currentFragment = (obj as Fragment)
+        }
+        super.setPrimaryItem(container, position, obj)
+    }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
index 77ff1df0c..41e65ae4e 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
@@ -16,7 +16,6 @@ import android.view.KeyEvent
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import android.view.ViewTreeObserver
 import android.view.ViewTreeObserver.OnGlobalLayoutListener
 import android.widget.ArrayAdapter
 import android.widget.Button
@@ -75,11 +74,9 @@ import fr.free.nrw.commons.BuildConfig
 import fr.free.nrw.commons.CameraPosition
 import fr.free.nrw.commons.CommonsApplication
 import fr.free.nrw.commons.CommonsApplication.Companion.instance
-import fr.free.nrw.commons.locationpicker.LocationPicker
 import fr.free.nrw.commons.Media
 import fr.free.nrw.commons.MediaDataExtractor
 import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
 import fr.free.nrw.commons.actions.ThanksClient
 import fr.free.nrw.commons.auth.SessionManager
 import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
@@ -103,7 +100,7 @@ import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity
 import fr.free.nrw.commons.kvstore.JsonKvStore
 import fr.free.nrw.commons.language.AppLanguageLookUpTable
 import fr.free.nrw.commons.location.LocationServiceManager
-import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
+import fr.free.nrw.commons.locationpicker.LocationPicker
 import fr.free.nrw.commons.profile.ProfileActivity
 import fr.free.nrw.commons.review.ReviewHelper
 import fr.free.nrw.commons.settings.Prefs
@@ -117,8 +114,13 @@ import fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources
 import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE
 import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction
 import fr.free.nrw.commons.utils.PermissionUtils.hasPermission
+import fr.free.nrw.commons.utils.ViewUtil
 import fr.free.nrw.commons.utils.ViewUtil.showShortToast
 import fr.free.nrw.commons.utils.ViewUtilWrapper
+import fr.free.nrw.commons.utils.copyToClipboard
+import fr.free.nrw.commons.utils.handleGeoCoordinates
+import fr.free.nrw.commons.utils.handleWebUrl
+import fr.free.nrw.commons.utils.setUnderlinedText
 import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage.Revision
 import io.reactivex.Observable
 import io.reactivex.Single
@@ -126,6 +128,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.schedulers.Schedulers
 import org.apache.commons.lang3.StringUtils
 import timber.log.Timber
+import java.lang.String.format
 import java.util.Date
 import java.util.Locale
 import java.util.Objects
@@ -315,14 +318,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
         _binding = FragmentMediaDetailBinding.inflate(inflater, container, false)
         val view: View = binding.root
 
-
-        Utils.setUnderlinedText(binding.seeMore, R.string.nominated_see_more, requireContext())
-
-        if (isCategoryImage) {
-            binding.authorLinearLayout.visibility = View.VISIBLE
-        } else {
-            binding.authorLinearLayout.visibility = View.GONE
-        }
+        binding.seeMore.setUnderlinedText(R.string.nominated_see_more)
 
         if (!sessionManager.isUserLoggedIn) {
             binding.categoryEditButton.visibility = View.GONE
@@ -545,6 +541,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
             }
         )
         binding.progressBarEdit.visibility = View.GONE
+        binding.descriptionEdit.visibility = View.VISIBLE
     }
 
     override fun onConfigurationChanged(newConfig: Configuration) {
@@ -622,10 +619,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(
-                    { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) },
+                    { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) },
                     { t: Throwable? -> Timber.e(t) })
         )
-        // compositeDisposable.add(disposable);
     }
 
     private fun onDiscussionLoaded(discussion: String) {
@@ -655,7 +651,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
         }
     }
 
-    private fun onDepictionsLoaded(idAndCaptions: List) {
+    private fun onDepictionsLoaded(idAndCaptions: List) {
         binding.depictsLayout.visibility = View.VISIBLE
         binding.depictionsEditButton.visibility = View.VISIBLE
         buildDepictionList(idAndCaptions)
@@ -813,10 +809,27 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
         categoryNames.clear()
         categoryNames.addAll(media.categories!!)
 
-        if (media.author == null || media.author == "") {
-            binding.authorLinearLayout.visibility = View.GONE
-        } else {
-            binding.mediaDetailAuthor.text = media.author
+        // Show author or uploader information for licensing compliance
+        val authorName = media.getAttributedAuthor()
+        val uploaderName = media.user
+        
+        when {
+            !authorName.isNullOrEmpty() -> {
+                // Show author if available
+                binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_author)
+                binding.mediaDetailAuthor.text = authorName
+                binding.authorLinearLayout.visibility = View.VISIBLE
+            }
+            !uploaderName.isNullOrEmpty() -> {
+                // Show uploader as fallback
+                binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_uploader)
+                binding.mediaDetailAuthor.text = uploaderName
+                binding.authorLinearLayout.visibility = View.VISIBLE
+            }
+            else -> {
+                // Hide if neither author nor uploader is available
+                binding.authorLinearLayout.visibility = View.GONE
+            }
         }
     }
 
@@ -865,24 +878,24 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
      * Populates media details fragment with depiction list
      * @param idAndCaptions
      */
-    private fun buildDepictionList(idAndCaptions: List) {
+    private fun buildDepictionList(idAndCaptions: List) {
         binding.mediaDetailDepictionContainer.removeAllViews()
 
         // Create a mutable list from the original list
         val mutableIdAndCaptions = idAndCaptions.toMutableList()
 
         if (mutableIdAndCaptions.isEmpty()) {
-            // Create a placeholder IdAndCaptions object and add it to the list
+            // Create a placeholder IdAndLabels object and add it to the list
             mutableIdAndCaptions.add(
-                IdAndCaptions(
+                IdAndLabels(
                     id = media?.pageId ?: "", // Use an empty string if media?.pageId is null
-                    captions = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
+                    labels = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
                 )
             )
         }
 
         val locale: String = Locale.getDefault().language
-        for (idAndCaption: IdAndCaptions in mutableIdAndCaptions) {
+        for (idAndCaption in mutableIdAndCaptions) {
             binding.mediaDetailDepictionContainer.addView(
                 buildDepictLabel(
                     getDepictionCaption(idAndCaption, locale),
@@ -894,22 +907,22 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
     }
 
 
-    private fun getDepictionCaption(idAndCaption: IdAndCaptions, locale: String): String? {
+    private fun getDepictionCaption(idAndCaption: IdAndLabels, locale: String): String? {
         // Check if the Depiction Caption is available in user's locale
         // if not then check for english, else show any available.
-        if (idAndCaption.captions[locale] != null) {
-            return idAndCaption.captions[locale]
+        if (idAndCaption.labels[locale] != null) {
+            return idAndCaption.labels[locale]
         }
-        if (idAndCaption.captions["en"] != null) {
-            return idAndCaption.captions["en"]
+        if (idAndCaption.labels["en"] != null) {
+            return idAndCaption.labels["en"]
         }
-        return idAndCaption.captions.values.iterator().next()
+        return idAndCaption.labels.values.iterator().next()
     }
 
     private fun onMediaDetailLicenceClicked() {
         val url: String? = media!!.licenseUrl
         if (!StringUtils.isBlank(url) && activity != null) {
-            Utils.handleWebUrl(activity, Uri.parse(url))
+            handleWebUrl(requireContext(), Uri.parse(url))
         } else {
             viewUtil.showShortToast(requireActivity(), getString(R.string.null_url))
         }
@@ -917,17 +930,17 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
 
     private fun onMediaDetailCoordinatesClicked() {
         if (media!!.coordinates != null && activity != null) {
-            Utils.handleGeoCoordinates(activity, media!!.coordinates)
+            handleGeoCoordinates(requireContext(), media!!.coordinates!!)
         }
     }
 
     private fun onCopyWikicodeClicked() {
         val data: String =
             "[[" + media!!.filename + "|thumb|" + media!!.fallbackDescription + "]]"
-        Utils.copy("wikiCode", data, context)
+        requireContext().copyToClipboard("wikiCode", data)
         Timber.d("Generated wikidata copy code: %s", data)
 
-        Toast.makeText(context, getString(R.string.wikicode_copied), Toast.LENGTH_SHORT)
+        Toast.makeText(requireContext(), getString(R.string.wikicode_copied), Toast.LENGTH_SHORT)
             .show()
     }
 
@@ -1014,12 +1027,12 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
         val message: String = if (result) {
             context.getString(
                 R.string.send_thank_success_message,
-                media!!.displayTitle
+                media!!.user
             )
         } else {
             context.getString(
                 R.string.send_thank_failure_message,
-                media!!.displayTitle
+                media!!.user
             )
         }
 
@@ -1648,7 +1661,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
                 getString(R.string.cancel),
                 {
                     val reason: String = input.text.toString()
-                    onDeleteClickeddialogtext(reason)
+                    onDeleteClickedDialogText(reason)
                 },
                 {},
                 input
@@ -1702,26 +1715,48 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
         resultSingle
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
-            .subscribe { _ ->
-                if (applicationKvStore.getBoolean(
-                        String.format(
-                            NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl
-                        ), false
-                    )
-                ) {
-                    applicationKvStore.remove(
-                        String.format(
-                            NOMINATING_FOR_DELETION_MEDIA,
-                            media!!.imageUrl
-                        )
-                    )
-                    callback!!.nominatingForDeletion(index)
-                }
-            }
+            .subscribe(this::handleDeletionResult, this::handleDeletionError);
+    }
+
+    /**
+     * Disables Progress Bar and Update delete button text.
+     */
+    private fun disableProgressBar() {
+        activity?.run {
+            runOnUiThread(Runnable {
+                binding.progressBarDeletion.visibility = View.GONE
+            })
+        } ?: return // Prevent NullPointerException when fragment is not attached to activity
+    }
+
+    private fun handleDeletionResult(success: Boolean) {
+        if (success) {
+            binding.nominateDeletion.text = getString(R.string.nominated_for_deletion_btn)
+            ViewUtil.showLongSnackbar(requireView(), getString(R.string.nominated_for_deletion))
+            disableProgressBar()
+            checkAndClearDeletionFlag()
+        } else {
+            disableProgressBar()
+        }
+    }
+
+    private fun handleDeletionError(throwable: Throwable) {
+        throwable.printStackTrace()
+        disableProgressBar()
+        checkAndClearDeletionFlag()
+    }
+
+    private fun checkAndClearDeletionFlag() {
+        if (applicationKvStore
+            .getBoolean(format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl), false)
+        ) {
+            applicationKvStore.remove(format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl))
+            callback!!.nominatingForDeletion(index)
+        }
     }
 
     @SuppressLint("CheckResult")
-    private fun onDeleteClickeddialogtext(reason: String) {
+    private fun onDeleteClickedDialogText(reason: String) {
         applicationKvStore.putBoolean(
             String.format(
                 NOMINATING_FOR_DELETION_MEDIA,
@@ -1738,27 +1773,12 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
         resultSingletext
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
-            .subscribe { _ ->
-                if (applicationKvStore.getBoolean(
-                        String.format(
-                            NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl
-                        ), false
-                    )
-                ) {
-                    applicationKvStore.remove(
-                        String.format(
-                            NOMINATING_FOR_DELETION_MEDIA,
-                            media!!.imageUrl
-                        )
-                    )
-                    callback!!.nominatingForDeletion(index)
-                }
-            }
+            .subscribe(this::handleDeletionResult, this::handleDeletionError);
     }
 
     private fun onSeeMoreClicked() {
         if (binding.nominatedDeletionBanner.visibility == View.VISIBLE && activity != null) {
-            Utils.handleWebUrl(activity, Uri.parse(media!!.pageTitle.mobileUri))
+            handleWebUrl(requireContext(), Uri.parse(media!!.pageTitle.mobileUri))
         }
     }
 
@@ -2109,22 +2129,17 @@ fun FileUsagesContainer(
     val uriHandle = LocalUriHandler.current
 
     Column(modifier = modifier) {
-
         Row(
             modifier = Modifier.fillMaxWidth(),
             verticalAlignment = Alignment.CenterVertically,
             horizontalArrangement = Arrangement.SpaceBetween
         ) {
-
             Text(
                 text = stringResource(R.string.usages_on_commons_heading),
                 textAlign = TextAlign.Center,
                 style = MaterialTheme.typography.titleSmall
             )
-
-            IconButton(onClick = {
-                isCommonsListExpanded = !isCommonsListExpanded
-            }) {
+            IconButton(onClick = { isCommonsListExpanded = !isCommonsListExpanded }) {
                 Icon(
                     imageVector = if (isCommonsListExpanded) Icons.Default.KeyboardArrowUp
                     else Icons.Default.KeyboardArrowDown,
@@ -2138,11 +2153,8 @@ fun FileUsagesContainer(
                 MediaDetailViewModel.FileUsagesContainerState.Loading -> {
                     LinearProgressIndicator()
                 }
-
                 is MediaDetailViewModel.FileUsagesContainerState.Success -> {
-
                     val data = commonsContainerState.data
-
                     if (data.isNullOrEmpty()) {
                         ListItem(headlineContent = {
                             Text(
@@ -2162,7 +2174,7 @@ fun FileUsagesContainer(
                                 headlineContent = {
                                     Text(
                                         modifier = Modifier.clickable {
-                                            uriHandle.openUri(usage.link!!)
+                                            usage.link?.let { uriHandle.openUri(it) }
                                         },
                                         text = usage.title,
                                         style = MaterialTheme.typography.titleSmall.copy(
@@ -2170,11 +2182,11 @@ fun FileUsagesContainer(
                                             textDecoration = TextDecoration.Underline
                                         )
                                     )
-                                })
+                                }
+                            )
                         }
                     }
                 }
-
                 is MediaDetailViewModel.FileUsagesContainerState.Error -> {
                     ListItem(headlineContent = {
                         Text(
@@ -2184,12 +2196,10 @@ fun FileUsagesContainer(
                         )
                     })
                 }
-
                 MediaDetailViewModel.FileUsagesContainerState.Initial -> {}
             }
         }
 
-
         Row(
             modifier = Modifier.fillMaxWidth(),
             verticalAlignment = Alignment.CenterVertically,
@@ -2200,10 +2210,7 @@ fun FileUsagesContainer(
                 textAlign = TextAlign.Center,
                 style = MaterialTheme.typography.titleSmall
             )
-
-            IconButton(onClick = {
-                isOtherWikisListExpanded = !isOtherWikisListExpanded
-            }) {
+            IconButton(onClick = { isOtherWikisListExpanded = !isOtherWikisListExpanded }) {
                 Icon(
                     imageVector = if (isOtherWikisListExpanded) Icons.Default.KeyboardArrowUp
                     else Icons.Default.KeyboardArrowDown,
@@ -2217,11 +2224,8 @@ fun FileUsagesContainer(
                 MediaDetailViewModel.FileUsagesContainerState.Loading -> {
                     LinearProgressIndicator()
                 }
-
                 is MediaDetailViewModel.FileUsagesContainerState.Success -> {
-
                     val data = globalContainerState.data
-
                     if (data.isNullOrEmpty()) {
                         ListItem(headlineContent = {
                             Text(
@@ -2240,16 +2244,20 @@ fun FileUsagesContainer(
                                 },
                                 headlineContent = {
                                     Text(
+                                        modifier = Modifier.clickable {
+                                            usage.link?.let { uriHandle.openUri(it) }
+                                        },
                                         text = usage.title,
                                         style = MaterialTheme.typography.titleSmall.copy(
+                                            color = Color(0xFF5A6AEC),
                                             textDecoration = TextDecoration.Underline
                                         )
                                     )
-                                })
+                                }
+                            )
                         }
                     }
                 }
-
                 is MediaDetailViewModel.FileUsagesContainerState.Error -> {
                     ListItem(headlineContent = {
                         Text(
@@ -2259,10 +2267,8 @@ fun FileUsagesContainer(
                         )
                     })
                 }
-
                 MediaDetailViewModel.FileUsagesContainerState.Initial -> {}
             }
         }
-
     }
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
deleted file mode 100644
index cba582a35..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
+++ /dev/null
@@ -1,678 +0,0 @@
-package fr.free.nrw.commons.media;
-
-import static fr.free.nrw.commons.Utils.handleWebUrl;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.widget.ProgressBar;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Toast;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentStatePagerAdapter;
-import androidx.viewpager.widget.ViewPager;
-import com.google.android.material.snackbar.Snackbar;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.bookmarks.models.Bookmark;
-import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
-import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.contributions.MainActivity;
-import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding;
-import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
-import fr.free.nrw.commons.profile.ProfileActivity;
-import fr.free.nrw.commons.utils.DownloadUtils;
-import fr.free.nrw.commons.utils.ImageUtils;
-import fr.free.nrw.commons.utils.NetworkUtils;
-import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.schedulers.Schedulers;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Objects;
-import java.util.concurrent.Callable;
-import javax.inject.Inject;
-import timber.log.Timber;
-
-public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener, MediaDetailFragment.Callback {
-
-    @Inject BookmarkPicturesDao bookmarkDao;
-
-    @Inject
-    protected OkHttpJsonApiClient okHttpJsonApiClient;
-
-    @Inject
-    protected SessionManager sessionManager;
-
-    private static CompositeDisposable compositeDisposable = new CompositeDisposable();
-
-    private FragmentMediaDetailPagerBinding binding;
-
-    private boolean editable;
-    private boolean isFeaturedImage;
-    private boolean isWikipediaButtonDisplayed;
-    MediaDetailAdapter adapter;
-    private Bookmark bookmark;
-    private MediaDetailProvider provider;
-    private boolean isFromFeaturedRootFragment;
-    private int position;
-
-    /**
-     * ProgressBar used to indicate the loading status of media items.
-     */
-    private ProgressBar imageProgressBar;
-
-    private ArrayList removedItems=new ArrayList();
-
-    public void clearRemoved(){
-        removedItems.clear();
-    }
-    public ArrayList getRemovedItems() {
-        return removedItems;
-    }
-
-
-    /**
-     * Use this factory method to create a new instance of this fragment using the provided
-     * parameters.
-     *
-     * This method will create a new instance of MediaDetailPagerFragment and the arguments will be
-     * saved to a bundle which will be later available in the {@link #onCreate(Bundle)}
-     * @param editable
-     * @param isFeaturedImage
-     * @return
-     */
-    public static MediaDetailPagerFragment newInstance(boolean editable, boolean isFeaturedImage) {
-        MediaDetailPagerFragment mediaDetailPagerFragment = new MediaDetailPagerFragment();
-        Bundle args = new Bundle();
-        args.putBoolean("is_editable", editable);
-        args.putBoolean("is_featured_image", isFeaturedImage);
-        mediaDetailPagerFragment.setArguments(args);
-        return mediaDetailPagerFragment;
-    }
-
-    public MediaDetailPagerFragment() {
-        // Required empty public constructor
-    };
-
-
-    @Override
-    public View onCreateView(LayoutInflater inflater,
-                             ViewGroup container,
-                             Bundle savedInstanceState) {
-        binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false);
-        binding.mediaDetailsPager.addOnPageChangeListener(this);
-        // Initialize the ProgressBar by finding it in the layout
-        imageProgressBar = binding.getRoot().findViewById(R.id.itemProgressBar);
-        adapter = new MediaDetailAdapter(getChildFragmentManager());
-
-        // ActionBar is now supported in both activities - if this crashes something is quite wrong
-        final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
-        if (actionBar != null) {
-            actionBar.setDisplayHomeAsUpEnabled(true);
-        }
-        else {
-            throw new AssertionError("Action bar should not be null!");
-        }
-
-        // If fragment is associated with ProfileActivity, then hide the tabLayout
-        if (getActivity() instanceof ProfileActivity) {
-            ((ProfileActivity)getActivity()).setTabLayoutVisibility(false);
-        }
-
-        // Else if fragment is associated with MainActivity then hide that tab layout
-        else if (getActivity() instanceof MainActivity) {
-            ((MainActivity)getActivity()).hideTabs();
-        }
-
-        binding.mediaDetailsPager.setAdapter(adapter);
-
-        if (savedInstanceState != null) {
-            final int pageNumber = savedInstanceState.getInt("current-page");
-            binding.mediaDetailsPager.setCurrentItem(pageNumber, false);
-            getActivity().invalidateOptionsMenu();
-        }
-        adapter.notifyDataSetChanged();
-
-        return binding.getRoot();
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem());
-        outState.putBoolean("editable", editable);
-        outState.putBoolean("isFeaturedImage", isFeaturedImage);
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        if (savedInstanceState != null) {
-            editable = savedInstanceState.getBoolean("editable", false);
-            isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false);
-
-        }
-        setHasOptionsMenu(true);
-        initProvider();
-    }
-
-    /**
-     * initialise the provider, based on from where the fragment was started, as in from an activity
-     * or a fragment
-     */
-    private void initProvider() {
-        if (getParentFragment() instanceof MediaDetailProvider) {
-            provider = (MediaDetailProvider) getParentFragment();
-        } else if (getActivity() instanceof MediaDetailProvider) {
-            provider = (MediaDetailProvider) getActivity();
-        } else {
-            throw new ClassCastException("Parent must implement MediaDetailProvider");
-        }
-    }
-
-    public MediaDetailProvider getMediaDetailProvider() {
-        return provider;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (getActivity() == null) {
-            Timber.d("Returning as activity is destroyed!");
-            return true;
-        }
-
-        Media m = provider.getMediaAtPosition(binding.mediaDetailsPager.getCurrentItem());
-        MediaDetailFragment mediaDetailFragment = this.adapter.getCurrentMediaDetailFragment();
-        switch (item.getItemId()) {
-            case R.id.menu_bookmark_current_image:
-                boolean bookmarkExists = bookmarkDao.updateBookmark(bookmark);
-                Snackbar snackbar = bookmarkExists ? Snackbar.make(getView(), R.string.add_bookmark, Snackbar.LENGTH_LONG) : Snackbar.make(getView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG);
-                snackbar.show();
-                updateBookmarkState(item);
-                return true;
-            case R.id.menu_copy_link:
-                String uri = m.getPageTitle().getCanonicalUri();
-                Utils.copy("shareLink", uri, requireContext());
-                Timber.d("Copied share link to clipboard: %s", uri);
-                Toast.makeText(requireContext(), getString(R.string.menu_link_copied),
-                    Toast.LENGTH_SHORT).show();
-                return true;
-            case R.id.menu_share_current_image:
-                Intent shareIntent = new Intent(Intent.ACTION_SEND);
-                shareIntent.setType("text/plain");
-                shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getPageTitle().getCanonicalUri());
-                startActivity(Intent.createChooser(shareIntent, "Share image via..."));
-
-                //Add media detail to backstack when the share button is clicked
-                //So that when the share is cancelled or completed the media detail page is on top
-                // of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
-                FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager();
-                if (supportFragmentManager.getBackStackEntryCount() < 2) {
-                    supportFragmentManager
-                        .beginTransaction()
-                        .addToBackStack(MediaDetailPagerFragment.class.getName())
-                        .commit();
-                    supportFragmentManager.executePendingTransactions();
-                }
-                return true;
-            case R.id.menu_browser_current_image:
-                // View in browser
-                handleWebUrl(requireContext(), Uri.parse(m.getPageTitle().getMobileUri()));
-                return true;
-            case R.id.menu_download_current_image:
-                // Download
-                if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
-                    ViewUtil.showShortSnackbar(getView(), R.string.no_internet);
-                    return false;
-                }
-                DownloadUtils.downloadMedia(getActivity(), m);
-                return true;
-            case R.id.menu_set_as_wallpaper:
-                // Set wallpaper
-                setWallpaper(m);
-                return true;
-            case R.id.menu_set_as_avatar:
-                // Set avatar
-                setAvatar(m);
-                return true;
-            case R.id.menu_view_user_page:
-                if (m != null && m.getUser() != null) {
-                    ProfileActivity.startYourself(getActivity(), m.getUser(),
-                        !Objects.equals(sessionManager.getUserName(), m.getUser()));
-                }
-                return true;
-            case R.id.menu_view_report:
-                showReportDialog(m);
-            case R.id.menu_view_set_white_background:
-                if (mediaDetailFragment != null) {
-                    mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.white));
-                }
-                return true;
-            case R.id.menu_view_set_black_background:
-                if (mediaDetailFragment != null) {
-                    mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.black));
-                }
-                return true;
-            default:
-                return super.onOptionsItemSelected(item);
-        }
-    }
-
-    private void showReportDialog(final Media media) {
-        if (media == null) {
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
-        final String[] values = requireContext().getResources()
-            .getStringArray(R.array.report_violation_options);
-        builder.setTitle(R.string.report_violation);
-        builder.setItems(R.array.report_violation_options, (dialog, which) -> {
-            sendReportEmail(media, values[which]);
-        });
-        builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
-        builder.setCancelable(false);
-        builder.show();
-    }
-
-    private void sendReportEmail(final Media media, final String type) {
-        final String technicalInfo = getTechInfo(media, type);
-
-        final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
-        feedbackIntent.setType("message/rfc822");
-        feedbackIntent.setData(Uri.parse("mailto:"));
-        feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
-            new String[]{CommonsApplication.REPORT_EMAIL});
-        feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
-            CommonsApplication.REPORT_EMAIL_SUBJECT);
-        feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo);
-        try {
-            startActivity(feedbackIntent);
-        } catch (final ActivityNotFoundException e) {
-            Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
-        }
-    }
-
-    private String getTechInfo(final Media media, final String type) {
-        final StringBuilder builder = new StringBuilder();
-
-        builder.append("Report type: ")
-            .append(type)
-            .append("\n\n");
-
-        builder.append("Image that you want to report: ")
-            .append(media.getImageUrl())
-            .append("\n\n");
-
-        builder.append("User that you want to report: ")
-            .append(media.getUser())
-            .append("\n\n");
-
-        if (sessionManager.getUserName() != null) {
-            builder.append("Your username: ")
-                .append(sessionManager.getUserName())
-                .append("\n\n");
-        }
-
-        builder.append("Violation reason: ")
-            .append("\n");
-
-        builder.append("----------------------------------------------")
-            .append("\n")
-            .append("(please write reason here)")
-            .append("\n")
-            .append("----------------------------------------------")
-            .append("\n\n")
-            .append("Thank you for your report! Our team will investigate as soon as possible.")
-            .append("\n")
-            .append("Please note that images also have a `Nominate for deletion` button.");
-
-        return builder.toString();
-    }
-
-    /**
-     * Set the media as the device's wallpaper if the imageUrl is not null
-     * Fails silently if setting the wallpaper fails
-     * @param media
-     */
-    private void setWallpaper(Media media) {
-        if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
-            Timber.d("Media URL not present");
-            return;
-        }
-        ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
-    }
-
-    /**
-     * Set the media as user's leaderboard avatar
-     * @param media
-     */
-    private void setAvatar(Media media) {
-        if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
-            Timber.d("Media URL not present");
-            return;
-        }
-        ImageUtils.setAvatarFromImageUrl(getActivity(), media.getImageUrl(),
-            Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
-            okHttpJsonApiClient, compositeDisposable);
-    }
-
-    @Override
-    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-        if (!editable) { // Disable menu options for editable views
-            menu.clear(); // see http://stackoverflow.com/a/8495697/17865
-            inflater.inflate(R.menu.fragment_image_detail, menu);
-            if (binding.mediaDetailsPager != null) {
-                MediaDetailProvider provider = getMediaDetailProvider();
-                if(provider == null) {
-                    return;
-                }
-                final int position;
-                if (isFromFeaturedRootFragment) {
-                    position = this.position;
-                } else {
-                    position = binding.mediaDetailsPager.getCurrentItem();
-                }
-
-                Media m = provider.getMediaAtPosition(position);
-                if (m != null) {
-                    // Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
-                    menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
-                    menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true);
-                    menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
-                    menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
-                    menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true);
-                    menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true);
-                    if (m.getUser() != null) {
-                        menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true);
-                    }
-
-                    try {
-                        URL mediaUrl = new URL(m.getImageUrl());
-                        this.handleBackgroundColorMenuItems(
-                            () -> BitmapFactory.decodeStream(mediaUrl.openConnection().getInputStream()),
-                            menu
-                        );
-                    } catch (Exception e) {
-                        Timber.e("Cant detect media transparency");
-                    }
-
-                    // Initialize bookmark object
-                    bookmark = new Bookmark(
-                            m.getFilename(),
-                            m.getAuthorOrUser(),
-                            BookmarkPicturesContentProvider.uriForName(m.getFilename())
-                    );
-                    updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image));
-                    final Integer contributionState = provider.getContributionStateAt(position);
-                    if (contributionState != null) {
-                        switch (contributionState) {
-                            case Contribution.STATE_FAILED:
-                            case Contribution.STATE_IN_PROGRESS:
-                            case Contribution.STATE_QUEUED:
-                                menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
-                                        .setVisible(false);
-                                menu.findItem(R.id.menu_copy_link).setEnabled(false)
-                                    .setVisible(false);
-                                menu.findItem(R.id.menu_share_current_image).setEnabled(false)
-                                        .setVisible(false);
-                                menu.findItem(R.id.menu_download_current_image).setEnabled(false)
-                                        .setVisible(false);
-                                menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
-                                        .setVisible(false);
-                                menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
-                                        .setVisible(false);
-                                break;
-                            case Contribution.STATE_COMPLETED:
-                                // Default set of menu items works fine. Treat same as regular media object
-                                break;
-                        }
-                    }
-                } else {
-                    menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
-                            .setVisible(false);
-                    menu.findItem(R.id.menu_copy_link).setEnabled(false)
-                        .setVisible(false);
-                    menu.findItem(R.id.menu_share_current_image).setEnabled(false)
-                            .setVisible(false);
-                    menu.findItem(R.id.menu_download_current_image).setEnabled(false)
-                            .setVisible(false);
-                    menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
-                            .setVisible(false);
-                    menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
-                            .setVisible(false);
-                }
-
-                if (!sessionManager.isUserLoggedIn()) {
-                    menu.findItem(R.id.menu_set_as_avatar).setVisible(false);
-                }
-
-            }
-        }
-    }
-
-    /**
-     * Decide wether or not we should display the background color menu items
-     * We display them if the image is transparent
-     * @param getBitmap
-     * @param menu
-     */
-    private void handleBackgroundColorMenuItems(Callable getBitmap, Menu menu) {
-        Observable.fromCallable(
-                getBitmap
-            ).subscribeOn(Schedulers.newThread())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(image -> {
-                if (image.hasAlpha()) {
-                    menu.findItem(R.id.menu_view_set_white_background).setVisible(true).setEnabled(true);
-                    menu.findItem(R.id.menu_view_set_black_background).setVisible(true).setEnabled(true);
-                }
-            });
-    }
-
-    private void updateBookmarkState(MenuItem item) {
-        boolean isBookmarked = bookmarkDao.findBookmark(bookmark);
-        if(isBookmarked) {
-            if(removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
-                removedItems.remove(new Integer(binding.mediaDetailsPager.getCurrentItem()));
-            }
-        }
-        else {
-            if(!removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
-                removedItems.add(binding.mediaDetailsPager.getCurrentItem());
-            }
-        }
-        int icon = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px : R.drawable.menu_ic_round_star_border_24px;
-        item.setIcon(icon);
-    }
-
-    public void showImage(int i, boolean isWikipediaButtonDisplayed) {
-        this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed;
-        setViewPagerCurrentItem(i);
-    }
-
-    public void showImage(int i) {
-        setViewPagerCurrentItem(i);
-    }
-
-    /**
-     * This function waits for the item to load then sets the item to current item
-     * @param position current item that to be shown
-     */
-    private void setViewPagerCurrentItem(int position) {
-
-        final Handler handler = new Handler(Looper.getMainLooper());
-        final Runnable runnable = new Runnable() {
-            @Override
-            public void run() {
-                // Show the ProgressBar while waiting for the item to load
-                imageProgressBar.setVisibility(View.VISIBLE);
-                // Check if the adapter has enough items loaded
-                if(adapter.getCount() > position){
-                    // Set the current item in the ViewPager
-                    binding.mediaDetailsPager.setCurrentItem(position, false);
-                    // Hide the ProgressBar once the item is loaded
-                    imageProgressBar.setVisibility(View.GONE);
-                } else {
-                    // If the item is not ready yet, post the Runnable again
-                    handler.post(this);
-                }
-            }
-        };
-        // Start the Runnable
-        handler.post(runnable);
-    }
-
-    /**
-     * The method notify the viewpager that number of items have changed.
-     */
-    public void notifyDataSetChanged(){
-        if (null != adapter) {
-            adapter.notifyDataSetChanged();
-        }
-    }
-
-    @Override
-    public void onPageScrolled(int i, float v, int i2) {
-        if(getActivity() == null) {
-            Timber.d("Returning as activity is destroyed!");
-            return;
-        }
-
-        getActivity().invalidateOptionsMenu();
-    }
-
-    @Override
-    public void onPageSelected(int i) {
-    }
-
-    @Override
-    public void onPageScrollStateChanged(int i) {
-    }
-
-    public void onDataSetChanged() {
-        if (null != adapter) {
-            adapter.notifyDataSetChanged();
-        }
-    }
-
-    /**
-     * Called after the media is nominated for deletion
-     *
-     * @param index item position that has been nominated
-     */
-    @Override
-    public void nominatingForDeletion(int index) {
-      provider.refreshNominatedMedia(index);
-    }
-
-    public interface MediaDetailProvider {
-        Media getMediaAtPosition(int i);
-
-        int getTotalMediaCount();
-
-        Integer getContributionStateAt(int position);
-
-        // Reload media detail fragment once media is nominated
-        void refreshNominatedMedia(int index);
-    }
-
-    //FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
-    private class MediaDetailAdapter extends FragmentStatePagerAdapter {
-
-        /**
-         * Keeps track of the current displayed fragment.
-         */
-        private Fragment mCurrentFragment;
-
-        public MediaDetailAdapter(FragmentManager fm) {
-            super(fm);
-        }
-
-        @Override
-        public Fragment getItem(int i) {
-            if (i == 0) {
-                // See bug https://code.google.com/p/android/issues/detail?id=27526
-                if(getActivity() == null) {
-                    Timber.d("Skipping getItem. Returning as activity is destroyed!");
-                    return null;
-                }
-                binding.mediaDetailsPager.postDelayed(() -> getActivity().invalidateOptionsMenu(), 5);
-            }
-            if (isFromFeaturedRootFragment) {
-                return MediaDetailFragment.forMedia(position+i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
-            } else {
-                return MediaDetailFragment.forMedia(i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
-            }
-        }
-
-        @Override
-        public int getCount() {
-            if (getActivity() == null) {
-                Timber.d("Skipping getCount. Returning as activity is destroyed!");
-                return 0;
-            }
-            return provider.getTotalMediaCount();
-        }
-
-        /**
-         * Get the currently displayed fragment.
-         * @return
-         */
-        public Fragment getCurrentFragment() {
-            return mCurrentFragment;
-        }
-
-        /**
-         * If current fragment is of type MediaDetailFragment, return it, otherwise return null.
-         * @return MediaDetailFragment
-         */
-        public MediaDetailFragment getCurrentMediaDetailFragment() {
-            if (mCurrentFragment instanceof MediaDetailFragment) {
-                return (MediaDetailFragment) mCurrentFragment;
-            }
-
-            return null;
-        }
-
-        /**
-         * Called to inform the adapter of which item is currently considered to be the "primary",
-         * that is the one show to the user as the current page.
-         * @param container
-         * @param position
-         * @param object
-         */
-        @Override
-        public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
-            @NonNull final Object object) {
-            // Update the current fragment if changed
-            if(getCurrentFragment() != object) {
-                mCurrentFragment = ((Fragment)object);
-            }
-            super.setPrimaryItem(container, position, object);
-        }
-    }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt
new file mode 100644
index 000000000..92cca611e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt
@@ -0,0 +1,622 @@
+package fr.free.nrw.commons.media
+
+import android.content.ActivityNotFoundException
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ProgressBar
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener
+import com.google.android.material.snackbar.Snackbar
+import fr.free.nrw.commons.CommonsApplication
+import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.auth.SessionManager
+import fr.free.nrw.commons.bookmarks.models.Bookmark
+import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
+import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
+import fr.free.nrw.commons.contributions.Contribution
+import fr.free.nrw.commons.contributions.MainActivity
+import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
+import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
+import fr.free.nrw.commons.profile.ProfileActivity
+import fr.free.nrw.commons.profile.ProfileActivity.Companion.startYourself
+import fr.free.nrw.commons.utils.ClipboardUtils.copy
+import fr.free.nrw.commons.utils.DownloadUtils.downloadMedia
+import fr.free.nrw.commons.utils.ImageUtils.setAvatarFromImageUrl
+import fr.free.nrw.commons.utils.ImageUtils.setWallpaperFromImageUrl
+import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
+import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
+import fr.free.nrw.commons.utils.handleWebUrl
+import io.reactivex.Observable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.functions.Consumer
+import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
+import java.net.URL
+import java.util.concurrent.Callable
+import javax.inject.Inject
+import androidx.core.net.toUri
+
+class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeListener,
+    MediaDetailFragment.Callback {
+    @JvmField
+    @Inject
+    var bookmarkDao: BookmarkPicturesDao? = null
+
+    @JvmField
+    @Inject
+    var okHttpJsonApiClient: OkHttpJsonApiClient? = null
+
+    @JvmField
+    @Inject
+    var sessionManager: SessionManager? = null
+
+    var binding: FragmentMediaDetailPagerBinding? = null
+    var editable: Boolean = false
+    var isFeaturedImage: Boolean = false
+    var isWikipediaButtonDisplayed: Boolean = false
+    var adapter: MediaDetailAdapter? = null
+    var bookmark: Bookmark? = null
+    var mediaDetailProvider: MediaDetailProvider? = null
+    var isFromFeaturedRootFragment: Boolean = false
+    var position: Int = 0
+
+    /**
+     * ProgressBar used to indicate the loading status of media items.
+     */
+    var imageProgressBar: ProgressBar? = null
+
+    var removedItems: ArrayList = ArrayList()
+
+    fun clearRemoved() = removedItems.clear()
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false)
+        binding!!.mediaDetailsPager.addOnPageChangeListener(this)
+        // Initialize the ProgressBar by finding it in the layout
+        imageProgressBar = binding!!.root.findViewById(R.id.itemProgressBar)
+        adapter = MediaDetailAdapter(this, childFragmentManager)
+
+        // ActionBar is now supported in both activities - if this crashes something is quite wrong
+        val actionBar = (activity as AppCompatActivity).supportActionBar
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true)
+        } else {
+            throw AssertionError("Action bar should not be null!")
+        }
+
+        // If fragment is associated with ProfileActivity, then hide the tabLayout
+        if (activity is ProfileActivity) {
+            (activity as ProfileActivity).setTabLayoutVisibility(false)
+        } else if (activity is MainActivity) {
+            (activity as MainActivity).hideTabs()
+        }
+
+        binding!!.mediaDetailsPager.adapter = adapter
+
+        if (savedInstanceState != null) {
+            val pageNumber = savedInstanceState.getInt("current-page")
+            binding!!.mediaDetailsPager.setCurrentItem(pageNumber, false)
+            requireActivity().invalidateOptionsMenu()
+        }
+        adapter!!.notifyDataSetChanged()
+
+        return binding!!.root
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        outState.putInt("current-page", binding!!.mediaDetailsPager.currentItem)
+        outState.putBoolean("editable", editable)
+        outState.putBoolean("isFeaturedImage", isFeaturedImage)
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (savedInstanceState != null) {
+            editable = savedInstanceState.getBoolean("editable", false)
+            isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false)
+        }
+        setHasOptionsMenu(true)
+        initProvider()
+    }
+
+    /**
+     * initialise the provider, based on from where the fragment was started, as in from an activity
+     * or a fragment
+     */
+    private fun initProvider() {
+        if (parentFragment is MediaDetailProvider) {
+            mediaDetailProvider = parentFragment as MediaDetailProvider
+        } else if (activity is MediaDetailProvider) {
+            mediaDetailProvider = activity as MediaDetailProvider?
+        } else {
+            throw ClassCastException("Parent must implement MediaDetailProvider")
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (activity == null) {
+            Timber.d("Returning as activity is destroyed!")
+            return true
+        }
+
+        val m = mediaDetailProvider!!.getMediaAtPosition(binding!!.mediaDetailsPager.currentItem)
+        val mediaDetailFragment = adapter!!.currentMediaDetailFragment
+        when (item.itemId) {
+            R.id.menu_bookmark_current_image -> {
+                val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark!!)
+                val snackbar = if (bookmarkExists) Snackbar.make(
+                    requireView(),
+                    R.string.add_bookmark,
+                    Snackbar.LENGTH_LONG
+                ) else Snackbar.make(
+                    requireView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG
+                )
+                snackbar.show()
+                updateBookmarkState(item)
+                return true
+            }
+
+            R.id.menu_copy_link -> {
+                val uri = m!!.pageTitle.canonicalUri
+                copy("shareLink", uri, requireContext())
+                Timber.d("Copied share link to clipboard: %s", uri)
+                Toast.makeText(
+                    requireContext(), getString(R.string.menu_link_copied),
+                    Toast.LENGTH_SHORT
+                ).show()
+                return true
+            }
+
+            R.id.menu_share_current_image -> {
+                val shareIntent = Intent(Intent.ACTION_SEND)
+                shareIntent.setType("text/plain")
+                shareIntent.putExtra(
+                    Intent.EXTRA_TEXT, """${m!!.displayTitle} 
+${m.pageTitle.canonicalUri}"""
+                )
+                startActivity(Intent.createChooser(shareIntent, "Share image via..."))
+
+                //Add media detail to backstack when the share button is clicked
+                //So that when the share is cancelled or completed the media detail page is on top
+                // of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
+                val supportFragmentManager = requireActivity().supportFragmentManager
+                if (supportFragmentManager.backStackEntryCount < 2) {
+                    supportFragmentManager
+                        .beginTransaction()
+                        .addToBackStack(MediaDetailPagerFragment::class.java.name)
+                        .commit()
+                    supportFragmentManager.executePendingTransactions()
+                }
+                return true
+            }
+
+            R.id.menu_browser_current_image -> {
+                // View in browser
+                handleWebUrl(requireContext(), m!!.pageTitle.mobileUri.toUri())
+                return true
+            }
+
+            R.id.menu_download_current_image -> {
+                // Download
+                if (!isInternetConnectionEstablished(activity)) {
+                    showShortSnackbar(requireView(), R.string.no_internet)
+                    return false
+                }
+                downloadMedia(activity, m!!)
+                return true
+            }
+
+            R.id.menu_set_as_wallpaper -> {
+                // Set wallpaper
+                setWallpaper(m!!)
+                return true
+            }
+
+            R.id.menu_set_as_avatar -> {
+                // Set avatar
+                setAvatar(m!!)
+                return true
+            }
+
+            R.id.menu_view_user_page -> {
+                if (m?.user != null) {
+                    startYourself(
+                        requireActivity(), m.user!!,
+                        sessionManager!!.userName != m.user
+                    )
+                }
+                return true
+            }
+
+            R.id.menu_view_report -> {
+                showReportDialog(m)
+                mediaDetailFragment?.onImageBackgroundChanged(
+                    ContextCompat.getColor(
+                        requireContext(),
+                        R.color.white
+                    )
+                )
+                return true
+            }
+
+            R.id.menu_view_set_white_background -> {
+                mediaDetailFragment?.onImageBackgroundChanged(
+                    ContextCompat.getColor(
+                        requireContext(),
+                        R.color.white
+                    )
+                )
+                return true
+            }
+
+            R.id.menu_view_set_black_background -> {
+                mediaDetailFragment?.onImageBackgroundChanged(
+                    ContextCompat.getColor(
+                        requireContext(),
+                        R.color.black
+                    )
+                )
+                return true
+            }
+
+            else -> return super.onOptionsItemSelected(item)
+        }
+    }
+
+    private fun showReportDialog(media: Media?) {
+        if (media == null) {
+            return
+        }
+        val builder = AlertDialog.Builder(requireActivity())
+        val values = requireContext().resources
+            .getStringArray(R.array.report_violation_options)
+        builder.setTitle(R.string.report_violation)
+        builder.setItems(
+            R.array.report_violation_options
+        ) { dialog: DialogInterface?, which: Int ->
+            sendReportEmail(media, values[which])
+        }
+        builder.setNegativeButton(
+            R.string.cancel
+        ) { dialog: DialogInterface?, which: Int -> }
+        builder.setCancelable(false)
+        builder.show()
+    }
+
+    private fun sendReportEmail(media: Media, type: String) {
+        val technicalInfo = getTechInfo(media, type)
+
+        val feedbackIntent = Intent(Intent.ACTION_SENDTO)
+        feedbackIntent.setType("message/rfc822")
+        feedbackIntent.setData(Uri.parse("mailto:"))
+        feedbackIntent.putExtra(
+            Intent.EXTRA_EMAIL,
+            arrayOf(CommonsApplication.REPORT_EMAIL)
+        )
+        feedbackIntent.putExtra(
+            Intent.EXTRA_SUBJECT,
+            CommonsApplication.REPORT_EMAIL_SUBJECT
+        )
+        feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo)
+        try {
+            startActivity(feedbackIntent)
+        } catch (e: ActivityNotFoundException) {
+            Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show()
+        }
+    }
+
+    private fun getTechInfo(media: Media, type: String): String {
+        val builder = StringBuilder()
+
+        builder.append("Report type: ")
+            .append(type)
+            .append("\n\n")
+
+        builder.append("Image that you want to report: ")
+            .append(media.imageUrl)
+            .append("\n\n")
+
+        builder.append("User that you want to report: ")
+            .append(media.user)
+            .append("\n\n")
+
+        if (sessionManager!!.userName != null) {
+            builder.append("Your username: ")
+                .append(sessionManager!!.userName)
+                .append("\n\n")
+        }
+
+        builder.append("Violation reason: ")
+            .append("\n")
+
+        builder.append("----------------------------------------------")
+            .append("\n")
+            .append("(please write reason here)")
+            .append("\n")
+            .append("----------------------------------------------")
+            .append("\n\n")
+            .append("Thank you for your report! Our team will investigate as soon as possible.")
+            .append("\n")
+            .append("Please note that images also have a `Nominate for deletion` button.")
+
+        return builder.toString()
+    }
+
+    /**
+     * Set the media as the device's wallpaper if the imageUrl is not null
+     * Fails silently if setting the wallpaper fails
+     * @param media
+     */
+    private fun setWallpaper(media: Media) {
+        if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
+            Timber.d("Media URL not present")
+            return
+        }
+        setWallpaperFromImageUrl(requireActivity(), media.imageUrl!!.toUri())
+    }
+
+    /**
+     * Set the media as user's leaderboard avatar
+     * @param media
+     */
+    private fun setAvatar(media: Media) {
+        if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
+            Timber.d("Media URL not present")
+            return
+        }
+        setAvatarFromImageUrl(
+            requireActivity(), media.imageUrl!!,
+            sessionManager!!.currentAccount!!.name,
+            okHttpJsonApiClient!!, Companion.compositeDisposable
+        )
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        if (!editable) { // Disable menu options for editable views
+            menu.clear() // see http://stackoverflow.com/a/8495697/17865
+            inflater.inflate(R.menu.fragment_image_detail, menu)
+            if (binding!!.mediaDetailsPager != null) {
+                val provider = mediaDetailProvider ?: return
+                val position = if (isFromFeaturedRootFragment) {
+                    position
+                } else {
+                    binding!!.mediaDetailsPager.currentItem
+                }
+
+                val m = provider.getMediaAtPosition(position)
+                if (m != null) {
+                    // Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
+                    menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true)
+                    menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true)
+                    menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true)
+                    menu.findItem(R.id.menu_download_current_image).setEnabled(true)
+                        .setVisible(true)
+                    menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true)
+                        .setVisible(true)
+                    menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true)
+                    if (m.user != null) {
+                        menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true)
+                    }
+
+                    try {
+                        val mediaUrl = URL(m.imageUrl)
+                        handleBackgroundColorMenuItems({
+                            BitmapFactory.decodeStream(
+                                mediaUrl.openConnection().getInputStream()
+                            )
+                        }, menu)
+                    } catch (e: Exception) {
+                        Timber.e("Cant detect media transparency")
+                    }
+
+                    // Initialize bookmark object
+                    bookmark = Bookmark(
+                        m.filename,
+                        m.getAuthorOrUser(),
+                        BookmarkPicturesContentProvider.uriForName(m.filename!!)
+                    )
+                    updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image))
+                    val contributionState = provider.getContributionStateAt(position)
+                    if (contributionState != null) {
+                        when (contributionState) {
+                            Contribution.STATE_FAILED, Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED -> {
+                                menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
+                                    .setVisible(false)
+                                menu.findItem(R.id.menu_copy_link).setEnabled(false)
+                                    .setVisible(false)
+                                menu.findItem(R.id.menu_share_current_image).setEnabled(false)
+                                    .setVisible(false)
+                                menu.findItem(R.id.menu_download_current_image).setEnabled(false)
+                                    .setVisible(false)
+                                menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
+                                    .setVisible(false)
+                                menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
+                                    .setVisible(false)
+                            }
+
+                            Contribution.STATE_COMPLETED -> {}
+                        }
+                    }
+                } else {
+                    menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
+                        .setVisible(false)
+                    menu.findItem(R.id.menu_copy_link).setEnabled(false)
+                        .setVisible(false)
+                    menu.findItem(R.id.menu_share_current_image).setEnabled(false)
+                        .setVisible(false)
+                    menu.findItem(R.id.menu_download_current_image).setEnabled(false)
+                        .setVisible(false)
+                    menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
+                        .setVisible(false)
+                    menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
+                        .setVisible(false)
+                }
+
+                if (!sessionManager!!.isUserLoggedIn) {
+                    menu.findItem(R.id.menu_set_as_avatar).setVisible(false)
+                }
+            }
+        }
+    }
+
+    /**
+     * Decide wether or not we should display the background color menu items
+     * We display them if the image is transparent
+     * @param getBitmap
+     * @param menu
+     */
+    private fun handleBackgroundColorMenuItems(getBitmap: Callable, menu: Menu) {
+        Observable.fromCallable(
+            getBitmap
+        ).subscribeOn(Schedulers.newThread())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(Consumer { image: Bitmap ->
+                if (image.hasAlpha()) {
+                    menu.findItem(R.id.menu_view_set_white_background).setVisible(true)
+                        .setEnabled(true)
+                    menu.findItem(R.id.menu_view_set_black_background).setVisible(true)
+                        .setEnabled(true)
+                }
+            })
+    }
+
+    private fun updateBookmarkState(item: MenuItem) {
+        val isBookmarked = bookmarkDao!!.findBookmark(bookmark)
+        if (isBookmarked) {
+            if (removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
+                removedItems.remove(binding!!.mediaDetailsPager.currentItem)
+            }
+        } else {
+            if (!removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
+                removedItems.add(binding!!.mediaDetailsPager.currentItem)
+            }
+        }
+
+        item.setIcon(if (isBookmarked) {
+            R.drawable.menu_ic_round_star_filled_24px
+        } else {
+            R.drawable.menu_ic_round_star_border_24px
+        })
+    }
+
+    fun showImage(i: Int, isWikipediaButtonDisplayed: Boolean) {
+        this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed
+        setViewPagerCurrentItem(i)
+    }
+
+    fun showImage(i: Int) {
+        setViewPagerCurrentItem(i)
+    }
+
+    /**
+     * This function waits for the item to load then sets the item to current item
+     * @param position current item that to be shown
+     */
+    private fun setViewPagerCurrentItem(position: Int) {
+        val handler = Handler(Looper.getMainLooper())
+        val runnable: Runnable = object : Runnable {
+            override fun run() {
+                // Show the ProgressBar while waiting for the item to load
+                imageProgressBar!!.visibility = View.VISIBLE
+                // Check if the adapter has enough items loaded
+                if (adapter!!.count > position) {
+                    // Set the current item in the ViewPager
+                    binding!!.mediaDetailsPager.setCurrentItem(position, false)
+                    // Hide the ProgressBar once the item is loaded
+                    imageProgressBar!!.visibility = View.GONE
+                } else {
+                    // If the item is not ready yet, post the Runnable again
+                    handler.post(this)
+                }
+            }
+        }
+        // Start the Runnable
+        handler.post(runnable)
+    }
+
+    /**
+     * The method notify the viewpager that number of items have changed.
+     */
+    fun notifyDataSetChanged() {
+        if (null != adapter) {
+            adapter!!.notifyDataSetChanged()
+        }
+    }
+
+    override fun onPageScrolled(i: Int, v: Float, i2: Int) {
+        if (activity == null) {
+            Timber.d("Returning as activity is destroyed!")
+            return
+        }
+
+        requireActivity().invalidateOptionsMenu()
+    }
+
+    override fun onPageSelected(i: Int) {
+    }
+
+    override fun onPageScrollStateChanged(i: Int) {
+    }
+
+    fun onDataSetChanged() {
+        if (null != adapter) {
+            adapter!!.notifyDataSetChanged()
+        }
+    }
+
+    /**
+     * Called after the media is nominated for deletion
+     *
+     * @param index item position that has been nominated
+     */
+    override fun nominatingForDeletion(index: Int) {
+        mediaDetailProvider!!.refreshNominatedMedia(index)
+    }
+
+    companion object {
+        private val compositeDisposable = CompositeDisposable()
+
+        /**
+         * Use this factory method to create a new instance of this fragment using the provided
+         * parameters.
+         *
+         * This method will create a new instance of MediaDetailPagerFragment and the arguments will be
+         * saved to a bundle which will be later available in the [.onCreate]
+         * @param editable
+         * @param isFeaturedImage
+         * @return
+         */
+        @JvmStatic
+        fun newInstance(editable: Boolean, isFeaturedImage: Boolean): MediaDetailPagerFragment {
+            val mediaDetailPagerFragment = MediaDetailPagerFragment()
+            val args = Bundle()
+            args.putBoolean("is_editable", editable)
+            args.putBoolean("is_featured_image", isFeaturedImage)
+            mediaDetailPagerFragment.arguments = args
+            return mediaDetailPagerFragment
+        }
+    }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt
new file mode 100644
index 000000000..591adfe75
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt
@@ -0,0 +1,14 @@
+package fr.free.nrw.commons.media
+
+import fr.free.nrw.commons.Media
+
+interface MediaDetailProvider {
+    fun getMediaAtPosition(i: Int): Media?
+
+    fun getTotalMediaCount(): Int
+
+    fun getContributionStateAt(position: Int): Int?
+
+    // Reload media detail fragment once media is nominated
+    fun refreshNominatedMedia(index: Int)
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
index ef0ef1f9c..d6c87410a 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
@@ -1,6 +1,6 @@
 package fr.free.nrw.commons.media
 
-import fr.free.nrw.commons.OkHttpConnectionFactory.UnsuccessfulResponseInterceptor.SUPPRESS_ERROR_LOG_HEADER
+import fr.free.nrw.commons.SUPPRESS_ERROR_LOG_HEADER
 import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
 import io.reactivex.Single
 import retrofit2.http.GET
@@ -186,13 +186,25 @@ interface MediaInterface {
     ): Single
 
     companion object {
+        /**
+         * Retrieved thumbnail height will be about this tall, but must be at least this height.
+         * A larger number means higher thumbnail resolution but more network usage.
+         */
+        const val THUMB_HEIGHT_PX = 450
+
         const val MEDIA_PARAMS =
-            "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
+            "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlheight=" +
+                    THUMB_HEIGHT_PX +
+                    "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|" +
+                    "ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
 
         /**
          * fetches category detail(title, hidden) for each category along with File information
          */
         const val MEDIA_PARAMS_WITH_CATEGORY_DETAILS =
-            "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
+            "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlheight=" +
+                    THUMB_HEIGHT_PX +
+                    "&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|" +
+                    "DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
     }
 }
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java
deleted file mode 100644
index 28df3811a..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package fr.free.nrw.commons.media;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import fr.free.nrw.commons.wikidata.mwapi.MwResponse;
-
-public class MwParseResponse extends MwResponse {
-    @Nullable
-    private MwParseResult parse;
-
-    @Nullable
-    public MwParseResult parse() {
-        return parse;
-    }
-
-    public boolean success() {
-        return parse != null;
-    }
-
-    @VisibleForTesting
-    protected void setParse(@Nullable MwParseResult parse) {
-        this.parse = parse;
-    }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt
new file mode 100644
index 000000000..fc0282a9e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt
@@ -0,0 +1,17 @@
+package fr.free.nrw.commons.media
+
+import androidx.annotation.VisibleForTesting
+import fr.free.nrw.commons.wikidata.mwapi.MwResponse
+
+class MwParseResponse : MwResponse() {
+    private var parse: MwParseResult? = null
+
+    fun parse(): MwParseResult? = parse
+
+    fun success(): Boolean = parse != null
+
+    @VisibleForTesting
+    protected fun setParse(parse: MwParseResult?) {
+        this.parse = parse
+    }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java
deleted file mode 100644
index edb7ff447..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package fr.free.nrw.commons.media;
-
-import com.google.gson.annotations.SerializedName;
-
-public class MwParseResult {
-    @SuppressWarnings("unused") private int pageid;
-    @SuppressWarnings("unused") private int index;
-    private MwParseText text;
-
-    public String text() {
-        return text.text;
-    }
-
-
-    public class MwParseText{
-        @SerializedName("*") private String text;
-    }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
new file mode 100644
index 000000000..7aacdea09
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
@@ -0,0 +1,18 @@
+package fr.free.nrw.commons.media
+
+import com.google.gson.annotations.SerializedName
+
+class MwParseResult {
+    private val pageid = 0
+    private val index = 0
+    private val text: MwParseText? = null
+
+    fun text(): String? {
+        return text?.text
+    }
+
+    inner class MwParseText {
+        @SerializedName("*")
+        internal val text: String? = null
+    }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt
index 291c834bd..a2f92c2e6 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt
@@ -281,6 +281,7 @@ class OkHttpJsonApiClient @Inject constructor(
                         FeedbackResponse::class.java
                     )
                 } catch (e: Exception) {
+                    e.printStackTrace()
                     return@fromCallable FeedbackResponse(0, 0, 0, FeaturedImages(0, 0), 0, "")
                 }
             }
@@ -531,40 +532,38 @@ ${"wd:" + place.wikiDataEntityId}"""
         )
         if (placeBindings != null) {
             for ((item1, label, location, clas) in placeBindings) {
-                if (item1 != null && label != null && clas != null) {
-                    val input = location.value
-                    val pattern = Pattern.compile(
-                        "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
-                    )
-                    val matcher = pattern.matcher(input)
+                val input = location.value
+                val pattern = Pattern.compile(
+                    "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
+                )
+                val matcher = pattern.matcher(input)
 
-                    if (matcher.find()) {
-                        val longStr = matcher.group(1)
-                        val latStr = matcher.group(2)
-                        val itemUrl = item1.value
-                        val itemName = label.value.replace("&", "&")
-                        val itemLatitude = latStr
-                        val itemLongitude = longStr
-                        val itemClass = clas.value
+                if (matcher.find()) {
+                    val longStr = matcher.group(1)
+                    val latStr = matcher.group(2)
+                    val itemUrl = item1.value
+                    val itemName = label.value.replace("&", "&")
+                    val itemLatitude = latStr
+                    val itemLongitude = longStr
+                    val itemClass = clas.value
 
-                        val formattedItemName =
-                            if (!itemClass.isEmpty())
-                                "$itemName ($itemClass)"
-                            else
-                                itemName
+                    val formattedItemName =
+                        if (!itemClass.isEmpty())
+                            "$itemName ($itemClass)"
+                        else
+                            itemName
 
-                        val kmlEntry = ("""
-        
-            $formattedItemName 
-            $itemUrl 
-            
-                $itemLongitude,$itemLatitude 
-             
-         """)
-                        kmlString = kmlString + kmlEntry
-                    } else {
-                        Timber.e("No match found")
-                    }
+                    val kmlEntry = ("""
+    
+        $formattedItemName 
+        $itemUrl 
+        
+            $itemLongitude,$itemLatitude 
+         
+     """)
+                    kmlString = kmlString + kmlEntry
+                } else {
+                    Timber.e("No match found")
                 }
             }
         }
@@ -589,37 +588,35 @@ ${"wd:" + place.wikiDataEntityId}"""
         val placeBindings = runQuery(leftLatLng, rightLatLng)
         if (placeBindings != null) {
             for ((item1, label, location, clas) in placeBindings) {
-                if (item1 != null && label != null && clas != null) {
-                    val input = location.value
-                    val pattern = Pattern.compile(
-                        "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
-                    )
-                    val matcher = pattern.matcher(input)
+                val input = location.value
+                val pattern = Pattern.compile(
+                    "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"
+                )
+                val matcher = pattern.matcher(input)
 
-                    if (matcher.find()) {
-                        val longStr = matcher.group(1)
-                        val latStr = matcher.group(2)
-                        val itemUrl = item1.value
-                        val itemName = label.value.replace("&", "&")
-                        val itemLatitude = latStr
-                        val itemLongitude = longStr
-                        val itemClass = clas.value
+                if (matcher.find()) {
+                    val longStr = matcher.group(1)
+                    val latStr = matcher.group(2)
+                    val itemUrl = item1.value
+                    val itemName = label.value.replace("&", "&")
+                    val itemLatitude = latStr
+                    val itemLongitude = longStr
+                    val itemClass = clas.value
 
-                        val formattedItemName = if (!itemClass.isEmpty())
-                            "$itemName ($itemClass)"
-                        else
-                            itemName
+                    val formattedItemName = if (!itemClass.isEmpty())
+                        "$itemName ($itemClass)"
+                    else
+                        itemName
 
-                        val gpxEntry =
-                            ("""
-    
-        $itemName 
-        $itemUrl 
-     """)
-                        gpxString = gpxString + gpxEntry
-                    } else {
-                        Timber.e("No match found")
-                    }
+                    val gpxEntry =
+                        ("""
+
+    $itemName 
+    $itemUrl 
+ """)
+                    gpxString = gpxString + gpxEntry
+                } else {
+                    Timber.e("No match found")
                 }
             }
         }
diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt
index 3f7a196fe..a4f08f241 100644
--- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt
@@ -18,7 +18,6 @@ import fr.free.nrw.commons.BuildConfig
 import fr.free.nrw.commons.CommonsApplication
 import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener
 import fr.free.nrw.commons.R
-import fr.free.nrw.commons.WelcomeActivity
 import fr.free.nrw.commons.actions.PageEditClient
 import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding
 import fr.free.nrw.commons.di.ApplicationlessInjection
@@ -32,6 +31,7 @@ import fr.free.nrw.commons.logging.CommonsLogSender
 import fr.free.nrw.commons.profile.ProfileActivity
 import fr.free.nrw.commons.review.ReviewActivity
 import fr.free.nrw.commons.settings.SettingsActivity
+import fr.free.nrw.commons.startWelcome
 import io.reactivex.Single
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.schedulers.Schedulers
@@ -114,13 +114,13 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
         val level = store.getString("userAchievementsLevel", "0")
         if (level == "0"){
             binding?.moreProfile?.text = getString(
-                R.string.profileLevel,
+                R.string.profile_withoutLevel,
                 getUserName(),
                 getString(R.string.see_your_achievements) // Second argument
             )
         } else {
             binding?.moreProfile?.text = getString(
-                R.string.profileLevel,
+                R.string.profile_withLevel,
                 getUserName(),
                 level
             )
@@ -241,7 +241,7 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
     }
 
     fun onTutorialClicked() {
-        WelcomeActivity.startYourself(requireActivity())
+        requireContext().startWelcome()
     }
 
     fun onSettingsClicked() {
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt
index a83d49f75..714cd388f 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/BottomSheetAdapter.kt
@@ -16,7 +16,7 @@ import fr.free.nrw.commons.nearby.model.BottomSheetItem
 /**
  * RecyclerView Adapter for displaying items in a bottom sheet.
  *
- * @property context The context used for inflating layout resources.
+ * @param context The context used for inflating layout resources.
  * @property itemList The list of BottomSheetItem objects to display.
  * @constructor Creates an instance of BottomSheetAdapter.
  */
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java b/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java
index db2c1f5d9..323f9756f 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/CheckBoxTriStates.java
@@ -44,7 +44,7 @@ public class CheckBoxTriStates extends AppCompatCheckBox {
         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
             switch (state) {
                 case UNKNOWN:
-                    setState(UNCHECKED);;
+                    setState(UNCHECKED);
                     break;
                 case UNCHECKED:
                     setState(CHECKED);
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java
index b5f760c9f..53e9970a6 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java
@@ -91,6 +91,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
             label.setSelected(!label.isSelected());
             holder.placeTypeLayout.setSelected(label.isSelected());
 
+            NearbyFilterState.setSelectedLabels(new ArrayList<>(selectedLabels));
             callback.filterByMarkerType(selectedLabels, 0, false, false);
         });
     }
@@ -152,6 +153,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
             label.setSelected(false);
             selectedLabels.remove(label);
         }
+        NearbyFilterState.setSelectedLabels(new ArrayList<>(selectedLabels));
         notifyDataSetChanged();
     }
 
@@ -163,6 +165,7 @@ public class NearbyFilterSearchRecyclerViewAdapter
                 selectedLabels.add(label);
             }
         }
+        NearbyFilterState.setSelectedLabels(new ArrayList<>(selectedLabels));
         notifyDataSetChanged();
     }
 
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java
index d3ece9bfa..d0aec96af 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterState.java
@@ -9,7 +9,7 @@ public class NearbyFilterState {
     private int checkBoxTriState;
     private ArrayList selectedLabels;
 
-    private static NearbyFilterState nearbyFılterStateInstance;
+    private static NearbyFilterState nearbyFilterStateInstance;
 
     /**
      * Define initial filter values here
@@ -23,10 +23,10 @@ public class NearbyFilterState {
     }
 
     public static NearbyFilterState getInstance() {
-        if (nearbyFılterStateInstance == null) {
-            nearbyFılterStateInstance = new NearbyFilterState();
+        if (nearbyFilterStateInstance == null) {
+            nearbyFilterStateInstance = new NearbyFilterState();
         }
-        return nearbyFılterStateInstance;
+        return nearbyFilterStateInstance;
     }
 
     public static void setSelectedLabels(ArrayList selectedLabels) {
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java
index caae8ee45..3bb2f549f 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java
@@ -1,10 +1,14 @@
 package fr.free.nrw.commons.nearby;
 
+import static java.util.Collections.emptyList;
+
 import android.location.Location;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import fr.free.nrw.commons.nearby.model.NearbyQueryParams;
+import fr.free.nrw.commons.nearby.model.NearbyQueryParams.Radial;
+import fr.free.nrw.commons.nearby.model.NearbyQueryParams.Rectangular;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -46,13 +50,14 @@ public class NearbyPlaces {
      * @param customQuery
      * @return list of places obtained
      */
+    @NonNull
     List radiusExpander(final LatLng currentLatLng, final String lang,
         final boolean returnClosestResult, @Nullable final String customQuery) throws Exception {
 
         final int minResults;
         final double maxRadius;
 
-        List places = Collections.emptyList();
+        List places = emptyList();
 
         // If returnClosestResult is true, then this means that we are trying to get closest point
         // to use in cardView in Contributions fragment
@@ -113,6 +118,7 @@ public class NearbyPlaces {
      * @return A list of places obtained from the Wikidata query.
      * @throws Exception If an error occurs during the retrieval process.
      */
+    @NonNull
     public List getFromWikidataQuery(
         final fr.free.nrw.commons.location.LatLng centerPoint,
         final fr.free.nrw.commons.location.LatLng screenTopRight,
@@ -120,11 +126,11 @@ public class NearbyPlaces {
         final boolean shouldQueryForMonuments,
         @Nullable final String customQuery) throws Exception {
         if (customQuery != null) {
-            return okHttpJsonApiClient
-                .getNearbyPlaces(
-                    new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
+            final List nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
+                    new Rectangular(screenTopRight, screenBottomLeft), lang,
                     shouldQueryForMonuments,
                     customQuery);
+            return nearbyPlaces != null ? nearbyPlaces : emptyList();
         }
 
         final int lowerLimit = 1000, upperLimit = 1500;
@@ -141,9 +147,10 @@ public class NearbyPlaces {
             final int itemCount = okHttpJsonApiClient.getNearbyItemCount(
                 new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft));
             if (itemCount < upperLimit) {
-                return okHttpJsonApiClient.getNearbyPlaces(
-                    new NearbyQueryParams.Rectangular(screenTopRight, screenBottomLeft), lang,
+                final List nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
+                    new Rectangular(screenTopRight, screenBottomLeft), lang,
                     shouldQueryForMonuments, null);
+                return nearbyPlaces != null ? nearbyPlaces : emptyList();
             }
         }
 
@@ -175,9 +182,10 @@ public class NearbyPlaces {
                 maxRadius = targetRadius - 1;
             }
         }
-        return okHttpJsonApiClient.getNearbyPlaces(
-            new NearbyQueryParams.Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
+        final List nearbyPlaces = okHttpJsonApiClient.getNearbyPlaces(
+            new Radial(centerPoint, targetRadius / 100f), lang, shouldQueryForMonuments,
             null);
+        return nearbyPlaces != null ? nearbyPlaces : emptyList();
     }
 
     /**
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java
index 3b3b798eb..cff2ed4de 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java
@@ -153,7 +153,10 @@ public class Place implements Parcelable {
                 .build(),
             item.getPic().getValue(),
             // Checking if the place exists or not
-            (item.getDestroyed().getValue() == "") && (item.getEndTime().getValue() == ""), entityId);
+            (item.getDestroyed().getValue() == "") && (item.getEndTime().getValue() == "")
+                && (item.getDateOfOfficialClosure().getValue() == "")
+                && (item.getPointInTime().getValue()==""),
+            entityId);
     }
 
     /**
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java
index fc01585ce..93bcba717 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java
@@ -105,9 +105,6 @@ public class Sitelinks implements Parcelable {
         private String commonsLink;
         private String wikipediaLink;
 
-        public Builder() {
-        }
-
         public Sitelinks.Builder setWikipediaLink(String link) {
             this.wikipediaLink = link;
             return this;
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
index e5196bee8..1775401dd 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
@@ -63,7 +63,10 @@ class WikidataFeedback : BaseActivity() {
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
 
         binding.appCompatButton.setOnClickListener {
-            var desc = findViewById(binding.radioGroup.checkedRadioButtonId).text
+            var desc = when (binding.radioGroup.checkedRadioButtonId) {
+                R.id.radioButton2 -> getString(R.string.is_at_a_different_place_wikidata, place)
+                else -> findViewById(binding.radioGroup.checkedRadioButtonId).text
+            }
             var det = binding.detailsEditText.text.toString()
             if (binding.radioGroup.checkedRadioButtonId == R.id.radioButton3 && binding.detailsEditText.text.isNullOrEmpty()) {
                 Toast
@@ -103,4 +106,4 @@ class WikidataFeedback : BaseActivity() {
         onBackPressed()
         return true
     }
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt
index 202f2c305..5f4d0ab13 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/CommonPlaceClickActions.kt
@@ -10,12 +10,13 @@ import androidx.activity.result.ActivityResultLauncher
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.widget.PopupMenu
 import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
 import fr.free.nrw.commons.auth.LoginActivity
 import fr.free.nrw.commons.contributions.ContributionController
 import fr.free.nrw.commons.kvstore.JsonKvStore
 import fr.free.nrw.commons.nearby.Place
 import fr.free.nrw.commons.utils.ActivityUtils
+import fr.free.nrw.commons.utils.handleGeoCoordinates
+import fr.free.nrw.commons.utils.handleWebUrl
 import fr.free.nrw.commons.wikidata.WikidataConstants
 import timber.log.Timber
 import javax.inject.Inject
@@ -104,7 +105,7 @@ class CommonPlaceClickActions
 
         fun onDirectionsClicked(): (Place) -> Unit =
             {
-                Utils.handleGeoCoordinates(activity, it.getLocation())
+                handleGeoCoordinates(activity, it.getLocation())
             }
 
         private fun storeSharedPrefs(selectedPlace: Place) {
@@ -113,7 +114,7 @@ class CommonPlaceClickActions
         }
 
         private fun openWebView(link: Uri): Boolean {
-            Utils.handleWebUrl(activity, link)
+            handleWebUrl(activity, link)
             return true
         }
 
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt
index 25baf3a92..3e6e71511 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt
@@ -58,7 +58,6 @@ import fr.free.nrw.commons.CommonsApplication
 import fr.free.nrw.commons.MapController.NearbyPlacesInfo
 import fr.free.nrw.commons.Media
 import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
 import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
 import fr.free.nrw.commons.contributions.ContributionController
 import fr.free.nrw.commons.contributions.MainActivity
@@ -75,7 +74,7 @@ import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
 import fr.free.nrw.commons.location.LocationUpdateListener
 import fr.free.nrw.commons.media.MediaClient
 import fr.free.nrw.commons.media.MediaDetailPagerFragment
-import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
+import fr.free.nrw.commons.media.MediaDetailProvider
 import fr.free.nrw.commons.navtab.NavTab
 import fr.free.nrw.commons.nearby.BottomSheetAdapter
 import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener
@@ -104,6 +103,10 @@ import fr.free.nrw.commons.utils.NearbyFABUtils.removeAnchorFromFAB
 import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
 import fr.free.nrw.commons.utils.SystemThemeUtils
 import fr.free.nrw.commons.utils.ViewUtil.showLongToast
+import fr.free.nrw.commons.utils.copyToClipboard
+import fr.free.nrw.commons.utils.handleGeoCoordinates
+import fr.free.nrw.commons.utils.handleWebUrl
+import fr.free.nrw.commons.utils.isMonumentsEnabled
 import fr.free.nrw.commons.wikidata.WikidataConstants
 import fr.free.nrw.commons.wikidata.WikidataEditListener
 import fr.free.nrw.commons.wikidata.WikidataEditListener.WikidataP18EditListener
@@ -122,6 +125,7 @@ import org.osmdroid.views.CustomZoomButtonsController
 import org.osmdroid.views.MapView
 import org.osmdroid.views.overlay.MapEventsOverlay
 import org.osmdroid.views.overlay.Marker
+import org.osmdroid.views.overlay.Overlay
 import org.osmdroid.views.overlay.ScaleBarOverlay
 import org.osmdroid.views.overlay.ScaleDiskOverlay
 import org.osmdroid.views.overlay.TilesOverlay
@@ -138,7 +142,6 @@ import java.util.UUID
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import javax.inject.Named
-import javax.sql.DataSource
 import kotlin.concurrent.Volatile
 
 
@@ -148,7 +151,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
     LocationUpdateListener,
     LocationPermissionCallback,
     ItemClickListener,
-    MediaDetailPagerFragment.MediaDetailProvider {
+    MediaDetailProvider {
     var binding: FragmentNearbyParentBinding? = null
 
     val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver {
@@ -266,6 +269,9 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
     private var dataList: MutableList? = null
     private var bottomSheetAdapter: BottomSheetAdapter? = null
 
+    private var userLocationOverlay: Overlay? = null
+    private var userLocationErrorOverlay: Overlay? = null
+
     private val galleryPickLauncherForResult =
         registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
             controller?.handleActivityResultWithCallback(
@@ -462,7 +468,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
                 }
             }
         _isDarkTheme = systemThemeUtils?.isDeviceInNightMode() == true
-        if (Utils.isMonumentsEnabled(Date())) {
+        if (isMonumentsEnabled) {
             binding?.rlContainerWlmMonthMessage?.visibility = View.VISIBLE
         } else {
             binding?.rlContainerWlmMonthMessage?.visibility = View.GONE
@@ -723,7 +729,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
             val targetP = GeoPoint(target.latitude, target.longitude)
             mapCenter = targetP
             binding?.map?.controller?.setCenter(targetP)
-            recenterMarkerToPosition(targetP)
+            updateUserLocationOverlays(targetP, true)
             if (!isCameFromExploreMap()) {
                 moveCameraToPosition(targetP)
             }
@@ -831,7 +837,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
         loadAnimations()
         setBottomSheetCallbacks()
         addActionToTitle()
-        if (!Utils.isMonumentsEnabled(Date())) {
+        if (!isMonumentsEnabled) {
             NearbyFilterState.setWlmSelected(false)
         }
     }
@@ -875,6 +881,12 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
     fun initNearbyFilter() {
         binding!!.nearbyFilterList.root.visibility = View.GONE
         hideBottomSheet()
+        binding!!.nearbyFilter.searchViewLayout.searchView.apply {
+            setIconifiedByDefault(false)
+            isIconified = false
+            setQuery("", false)
+            clearFocus()
+        }
         binding!!.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener { v, hasFocus ->
             setLayoutHeightAlignedToWidth(
                 1.25,
@@ -918,6 +930,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
                     return _isDarkTheme
                 }
             })
+        restoreStoredFilterSelection()
         binding!!.nearbyFilterList.root
             .layoutParams.width = getScreenWidth(
             requireActivity(),
@@ -936,6 +949,22 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
                 })
     }
 
+    private fun restoreStoredFilterSelection() {
+        val adapter = nearbyFilterSearchRecyclerViewAdapter ?: return
+        val savedLabels = ArrayList(NearbyFilterState.getInstance().selectedLabels)
+        adapter.selectedLabels.clear()
+        val savedSet = savedLabels.toSet()
+        Label.valuesAsList().forEach { label ->
+            val isSelected = savedSet.contains(label)
+            label.setSelected(isSelected)
+            if (isSelected) {
+                adapter.selectedLabels.add(label)
+            }
+        }
+        NearbyFilterState.setSelectedLabels(ArrayList(adapter.selectedLabels))
+        adapter.notifyDataSetChanged()
+    }
+
     override fun setCheckBoxAction() {
         binding!!.nearbyFilterList.checkboxTriStates.addAction()
         binding!!.nearbyFilterList.checkboxTriStates.state = CheckBoxTriStates.UNKNOWN
@@ -973,7 +1002,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
             } else if (bottomSheetDetailsBehavior!!.state
                 == BottomSheetBehavior.STATE_EXPANDED
             ) {
-                bottomSheetDetailsBehavior!!.state = BottomSheetBehavior.STATE_COLLAPSED
+                bottomSheetDetailsBehavior!!.setState(BottomSheetBehavior.STATE_COLLAPSED)
             }
         }
 
@@ -1012,11 +1041,10 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
      */
     private fun addActionToTitle() {
         binding!!.bottomSheetDetails.title.setOnLongClickListener { view ->
-            Utils.copy(
-                "place", binding!!.bottomSheetDetails.title.text.toString(),
-                context
+            requireContext().copyToClipboard(
+                "place", binding!!.bottomSheetDetails.title.text.toString()
             )
-            Toast.makeText(context, fr.free.nrw.commons.R.string.text_copy, Toast.LENGTH_SHORT)
+            Toast.makeText(requireContext(), fr.free.nrw.commons.R.string.text_copy, Toast.LENGTH_SHORT)
                 .show()
             true
         }
@@ -1064,7 +1092,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
 
     override fun updateListFragment(placeList: List) {
         adapter!!.clear()
-        adapter!!.items = placeList
+        adapter!!.items = placeList.filter{ it.name.isNotEmpty() }
         binding!!.bottomSheetNearby.noResultsMessage.visibility =
             if (placeList.isEmpty()) View.VISIBLE else View.GONE
     }
@@ -1575,7 +1603,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
                     searchLatLng,
                     false,
                     true,
-                    Utils.isMonumentsEnabled(Date()),
+                    isMonumentsEnabled,
                     customQuery
                 )
             }
@@ -1628,7 +1656,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
                     searchLatLng,
                     false,
                     true,
-                    Utils.isMonumentsEnabled(Date()),
+                    isMonumentsEnabled,
                     customQuery
                 )
             }
@@ -1756,9 +1784,9 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
     override fun animateFABs() {
         if (binding!!.fabPlus.isShown) {
             if (isFABsExpanded) {
-                collapseFABs(isFABsExpanded)
+                collapseFABs(true)
             } else {
-                expandFABs(isFABsExpanded)
+                expandFABs(false)
             }
         }
     }
@@ -1862,6 +1890,8 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
         lastKnownLocation = latLng
         NearbyController.currentLocation = lastKnownLocation
         presenter!!.updateMapAndList(locationChangeType)
+
+        updateUserLocationOverlays(GeoPoint(latLng.latitude, latLng.longitude), true)
     }
 
     override fun onLocationChangedSignificantly(latLng: LatLng) {
@@ -2013,17 +2043,17 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
                 if (place.exists && place.pic.trim { it <= ' ' }.isEmpty()) {
                     shouldUpdateMarker = true
                 }
-            } else if (displayExists && !displayNeedsPhoto) {
+            } else if (displayExists) {
                 // Exists and all included needs and doesn't needs photo
                 if (place.exists) {
                     shouldUpdateMarker = true
                 }
-            } else if (!displayExists && displayNeedsPhoto) {
+            } else if (displayNeedsPhoto) {
                 // All and only needs photo
                 if (place.pic.trim { it <= ' ' }.isEmpty()) {
                     shouldUpdateMarker = true
                 }
-            } else if (!displayExists && !displayNeedsPhoto) {
+            } else {
                 // all
                 shouldUpdateMarker = true
             }
@@ -2456,9 +2486,11 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
                 Timber.d("Gallery button tapped. Place: %s", selectedPlace.toString())
                 storeSharedPrefs(selectedPlace!!)
                 activity?.let {
+                    // Pass singleSelection = true for Nearby flow
                     controller!!.initiateCustomGalleryPickWithPermission(
                         it,
-                        customSelectorLauncherForResult
+                        customSelectorLauncherForResult,
+                        singleSelection = true
                     )
                 }
             }
@@ -2641,43 +2673,14 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
      */
     override fun clearAllMarkers() {
         binding!!.map.overlayManager.clear()
-        binding!!.map.invalidate()
-        val geoPoint = mapCenter
-        if (geoPoint != null) {
-            val diskOverlay =
-                ScaleDiskOverlay(
-                    this.context,
-                    geoPoint, 2000, UnitOfMeasure.foot
-                )
-            val circlePaint = Paint()
-            circlePaint.color = Color.rgb(128, 128, 128)
-            circlePaint.style = Paint.Style.STROKE
-            circlePaint.strokeWidth = 2f
-            diskOverlay.setCirclePaint2(circlePaint)
-            val diskPaint = Paint()
-            diskPaint.color = Color.argb(40, 128, 128, 128)
-            diskPaint.style = Paint.Style.FILL_AND_STROKE
-            diskOverlay.setCirclePaint1(diskPaint)
-            diskOverlay.setDisplaySizeMin(900)
-            diskOverlay.setDisplaySizeMax(1700)
-            binding!!.map.overlays.add(diskOverlay)
-            val startMarker = Marker(
-                binding!!.map
-            )
-            startMarker.position = geoPoint
-            startMarker.setAnchor(
-                Marker.ANCHOR_CENTER,
-                Marker.ANCHOR_BOTTOM
-            )
-            startMarker.icon =
-                getDrawable(
-                    this.requireContext(),
-                    fr.free.nrw.commons.R.drawable.current_location_marker
-                )
-            startMarker.title = "Your Location"
-            startMarker.textLabelFontSize = 24
-            binding!!.map.overlays.add(startMarker)
+
+        var geoPoint = mapCenter
+        val lastLatLng = locationManager.getLastLocation()
+        if (lastLatLng != null) {
+            geoPoint = GeoPoint(lastLatLng.latitude, lastLatLng.longitude)
         }
+        updateUserLocationOverlays(geoPoint, false)
+
         val scaleBarOverlay = ScaleBarOverlay(binding!!.map)
         scaleBarOverlay.setScaleBarOffset(15, 25)
         val barPaint = Paint()
@@ -2687,6 +2690,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
         binding!!.map.overlays.add(scaleBarOverlay)
         binding!!.map.overlays.add(mapEventsOverlay)
         binding!!.map.setMultiTouchControls(true)
+        binding!!.map.invalidate()
     }
 
     /**
@@ -2697,45 +2701,149 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
     private fun recenterMarkerToPosition(geoPoint: GeoPoint?) {
         geoPoint?.let {
             binding?.map?.controller?.setCenter(it)
-            val overlays = binding?.map?.overlays ?: return@let
 
-            // Remove markers and disks using index-based removal
-            var i = 0
-            while (i < overlays.size) {
-                when (overlays[i]) {
-                    is Marker, is ScaleDiskOverlay -> overlays.removeAt(i)
-                    else -> i++
-                }
-            }
-
-            // Add disk overlay
-            ScaleDiskOverlay(context, it, 2000, UnitOfMeasure.foot).apply {
-                setCirclePaint2(Paint().apply {
-                    color = Color.rgb(128, 128, 128)
-                    style = Paint.Style.STROKE
-                    strokeWidth = 2f
-                })
-                setCirclePaint1(Paint().apply {
-                    color = Color.argb(40, 128, 128, 128)
-                    style = Paint.Style.FILL_AND_STROKE
-                })
-                setDisplaySizeMin(900)
-                setDisplaySizeMax(1700)
-                overlays.add(this)
-            }
-
-            // Add marker
-            Marker(binding?.map).apply {
-                position = it
-                setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
-                icon = getDrawable(context, R.drawable.current_location_marker)
-                title = "Your Location"
-                textLabelFontSize = 24
-                overlays.add(this)
-            }
+            updateUserLocationOverlays(it, true);
         }
     }
 
+    /**
+     * Updates the user current location overlays (both the location and error overlays) by
+     * replacing any existing location overlays with new overlays at the given GeoPoint. If there
+     * are no existing location and error overlays, then new overlays are added.
+     *
+     * @param geoPoint The GeoPoint representing the user's current location.
+     * @param invalidate If true, the map overlays will be invalidated after the user
+     * location overlays are updated/added. If false, the overlays will not be invalidated.
+     */
+    private fun updateUserLocationOverlays(geoPoint: GeoPoint?, invalidate: Boolean) {
+        geoPoint?.let{
+            updateUserLocationOverlay(geoPoint)
+            updateUserLocationErrorOverlay(geoPoint)
+        }
+
+        if (invalidate) {
+            binding!!.map.invalidate()
+        }
+    }
+
+    /**
+     * Updates the user location error overlay by either replacing it with or adding a new one.
+     *
+     * If the user location error overlay is null, the new overlay is added. If the
+     * overlay is not null, it is replaced by the new overlay.
+     *
+     * @param geoPoint The GeoPoint representing the user's location
+     */
+    private fun updateUserLocationErrorOverlay(geoPoint: GeoPoint) {
+            val overlays = binding?.map?.overlays ?: return
+
+            // Multiply accuracy by 2 to get 95% confidence interval
+            val accuracy = getCurrentLocationAccuracy() * 2
+            val overlay = createCurrentLocationErrorOverlay(this.context, geoPoint,
+                (accuracy).toInt(), UnitOfMeasure.meter)
+
+            val index = overlays.indexOf(userLocationErrorOverlay)
+
+            if (userLocationErrorOverlay == null || index == -1) {
+                overlays.add(overlay)
+            } else {
+                overlays[index] = overlay
+            }
+
+            userLocationErrorOverlay = overlay
+    }
+
+    /**
+     * Updates the user location overlay by either replacing it with or adding a new one.
+     *
+     * If the user location overlay is null, the new overlay is added. If the
+     * overlay is not null, it is replaced by the new overlay.
+     *
+     * @param geoPoint The GeoPoint representing the user's location
+     */
+    private fun updateUserLocationOverlay(geoPoint: GeoPoint) {
+            val overlays = binding?.map?.overlays ?: return
+
+            val overlay = createCurrentLocationOverlay(geoPoint)
+
+            val index = overlays.indexOf(userLocationOverlay)
+
+            if (userLocationOverlay == null || index == -1) {
+                overlays.add(overlay)
+            } else {
+                overlays[index] = overlay
+            }
+
+            userLocationOverlay = overlay
+    }
+
+    /**
+     * @return The accuracy of the current location with a confidence at the 68th percentile.
+     * Units are in meters. Returning 0 may indicate failure.
+     */
+    private fun getCurrentLocationAccuracy(): Float {
+        var accuracy = 0f
+        val lastLocation = locationManager.getLastLocation()
+        if (lastLocation != null) {
+            accuracy = lastLocation.accuracy
+        }
+
+        return accuracy
+    }
+
+    /**
+     * Creates the current location overlay
+     *
+     * @param geoPoint The GeoPoint where the current location overlay will be placed.
+     *
+     * @return The current location overlay as a Marker
+     */
+    private fun createCurrentLocationOverlay(geoPoint: GeoPoint): Marker {
+        val currentLocationOverlay = Marker(
+            binding!!.map
+        )
+        currentLocationOverlay.position = geoPoint
+        currentLocationOverlay.icon =
+            getDrawable(
+                this.requireContext(),
+                fr.free.nrw.commons.R.drawable.current_location_marker
+            )
+        currentLocationOverlay.title = "Your Location"
+        currentLocationOverlay.textLabelFontSize = 24
+        currentLocationOverlay.setAnchor(0.5f, 0.5f)
+
+        return currentLocationOverlay
+    }
+
+    /**
+     * Creates the location error overlay to show the user how accurate the current location
+     * overlay is. The edge of the disk is the 95% confidence interval.
+     *
+     * @param context The Android context
+     * @param point The user's location as a GeoPoint
+     * @param value The radius of the disk
+     * @param unitOfMeasure The unit of measurement of the value/disk radius.
+     *
+     * @return The location error overlay as a ScaleDiskOverlay.
+     */
+    private fun createCurrentLocationErrorOverlay(context: Context?, point: GeoPoint, value: Int,
+                                           unitOfMeasure: UnitOfMeasure): ScaleDiskOverlay {
+        val scaleDisk = ScaleDiskOverlay(context, point, value, unitOfMeasure)
+
+        scaleDisk.setCirclePaint2(Paint().apply {
+            color = Color.rgb(128, 128, 128)
+            style = Paint.Style.STROKE
+            strokeWidth = 2f
+        })
+
+        scaleDisk.setCirclePaint1(Paint().apply {
+            color = Color.argb(40, 128, 128, 128)
+            style = Paint.Style.FILL_AND_STROKE
+        })
+
+        return scaleDisk
+    }
+
     private fun moveCameraToPosition(geoPoint: GeoPoint) {
         binding!!.map.controller.animateTo(geoPoint)
     }
@@ -2769,14 +2877,14 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
 
             R.drawable.ic_directions_black_24dp -> {
                 selectedPlace?.let {
-                    Utils.handleGeoCoordinates(this.context, it.getLocation())
+                    handleGeoCoordinates(requireContext(), it.getLocation())
                     binding?.map?.zoomLevelDouble ?: 0.0
                 }
             }
 
             R.drawable.ic_wikidata_logo_24dp -> {
                 selectedPlace?.siteLinks?.wikidataLink?.let {
-                    Utils.handleWebUrl(this.context, it)
+                    handleWebUrl(requireContext(), it)
                 }
             }
 
@@ -2794,13 +2902,13 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
 
             R.drawable.ic_wikipedia_logo_24dp -> {
                 selectedPlace?.siteLinks?.wikipediaLink?.let {
-                    Utils.handleWebUrl(this.context, it)
+                    handleWebUrl(requireContext(), it)
                 }
             }
 
             R.drawable.ic_commons_icon_vector -> {
                 selectedPlace?.siteLinks?.commonsLink?.let {
-                    Utils.handleWebUrl(this.context, it)
+                    handleWebUrl(requireContext(), it)
                 }
             }
 
@@ -2902,4 +3010,4 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
             return input.contains("(") || input.contains(")")
         }
     }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
index f28cc833e..1d5d7bd80 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
@@ -7,7 +7,8 @@ class NearbyResultItem(
     private val wikipediaArticle: ResultTuple?,
     private val commonsArticle: ResultTuple?,
     private val location: ResultTuple?,
-    private val label: ResultTuple?,
+    @field:SerializedName("label") private val label: ResultTuple?,
+    @field:SerializedName("itemLabel") private val itemLabel: ResultTuple?,
     @field:SerializedName("streetAddress") private val address: ResultTuple?,
     private val icon: ResultTuple?,
     @field:SerializedName("class") private val className: ResultTuple?,
@@ -18,6 +19,8 @@ class NearbyResultItem(
     @field:SerializedName("description") private val description: ResultTuple?,
     @field:SerializedName("endTime") private val endTime: ResultTuple?,
     @field:SerializedName("monument") private val monument: ResultTuple?,
+    @field:SerializedName("dateOfOfficialClosure") private val dateOfOfficialClosure: ResultTuple?,
+    @field:SerializedName("pointInTime") private val pointInTime: ResultTuple?,
 ) {
     fun getItem(): ResultTuple = item ?: ResultTuple()
 
@@ -27,7 +30,15 @@ class NearbyResultItem(
 
     fun getLocation(): ResultTuple = location ?: ResultTuple()
 
-    fun getLabel(): ResultTuple = label ?: ResultTuple()
+    /**
+     * Returns label for display (pins, popup), using fallback to itemLabel if needed.
+     */
+    fun getLabel(): ResultTuple = label ?: itemLabel ?: ResultTuple()
+
+    /**
+     * Returns only the original label field, for Wikidata edits.
+     */
+    fun getOriginalLabel(): ResultTuple = label ?: ResultTuple()
 
     fun getIcon(): ResultTuple = icon ?: ResultTuple()
 
@@ -41,6 +52,8 @@ class NearbyResultItem(
 
     fun getDestroyed(): ResultTuple = destroyed ?: ResultTuple()
 
+    fun getDateOfOfficialClosure(): ResultTuple = dateOfOfficialClosure ?: ResultTuple()
+
     fun getDescription(): ResultTuple = description ?: ResultTuple()
 
     fun getEndTime(): ResultTuple = endTime ?: ResultTuple()
@@ -48,4 +61,7 @@ class NearbyResultItem(
     fun getAddress(): String = address?.value ?: ""
 
     fun getMonument(): ResultTuple? = monument
+
+    fun getPointInTime(): ResultTuple = pointInTime ?: ResultTuple()
+
 }
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt
index b4639b14a..6ec1064be 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.kt
@@ -351,6 +351,7 @@ class NearbyParentFragmentPresenter
                         pic = repoPlace.pic ?: ""
                         exists = repoPlace.exists ?: true
                         longDescription = repoPlace.longDescription ?: ""
+                        language = repoPlace.language
                     }
                 } else {
                     indicesToUpdate.add(i)
diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt
index 1547f89ad..4a43bf470 100644
--- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt
@@ -8,20 +8,23 @@ import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
+import androidx.core.view.ViewGroupCompat
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.google.android.material.snackbar.Snackbar
 import fr.free.nrw.commons.CommonsApplication
 import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
 import fr.free.nrw.commons.auth.SessionManager
 import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
 import fr.free.nrw.commons.databinding.ActivityNotificationBinding
 import fr.free.nrw.commons.notification.models.Notification
 import fr.free.nrw.commons.notification.models.NotificationType
 import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets
 import fr.free.nrw.commons.utils.NetworkUtils
 import fr.free.nrw.commons.utils.ViewUtil
+import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets
+import fr.free.nrw.commons.utils.handleWebUrl
 import io.reactivex.Observable
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.schedulers.Schedulers
@@ -56,6 +59,9 @@ class NotificationActivity : BaseActivity() {
         super.onCreate(savedInstanceState)
         isRead = intent.getStringExtra("title") == "read"
         binding = ActivityNotificationBinding.inflate(layoutInflater)
+        ViewGroupCompat.installCompatInsetsDispatch(binding.root)
+        applyEdgeToEdgeTopInsets(binding.toolbar.toolbar)
+        binding.listView.applyEdgeToEdgeBottomPaddingInsets()
         setContentView(binding.root)
         mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag(
             tagNotificationWorkerFragment
@@ -197,7 +203,7 @@ class NotificationActivity : BaseActivity() {
 
     private fun handleUrl(url: String?) {
         if (url.isNullOrEmpty()) return
-        Utils.handleWebUrl(this, Uri.parse(url))
+        handleWebUrl(this, Uri.parse(url))
     }
 
     private fun setItems(notificationList: List?) {
diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
index 164842c9a..8567d37ae 100644
--- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
@@ -3,16 +3,17 @@ package fr.free.nrw.commons.profile
 import android.content.Context
 import android.content.Intent
 import android.graphics.Bitmap
-import android.net.Uri
 import android.os.Bundle
 import android.util.Log
-import android.view.*
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.core.content.FileProvider
+import androidx.core.os.bundleOf
 import androidx.fragment.app.Fragment
 import fr.free.nrw.commons.R
-import fr.free.nrw.commons.Utils
 import fr.free.nrw.commons.ViewPagerAdapter
 import fr.free.nrw.commons.auth.SessionManager
 import fr.free.nrw.commons.contributions.ContributionsFragment
@@ -20,11 +21,13 @@ import fr.free.nrw.commons.databinding.ActivityProfileBinding
 import fr.free.nrw.commons.profile.achievements.AchievementsFragment
 import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment
 import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
 import fr.free.nrw.commons.utils.DialogUtil
 import java.io.File
 import java.io.FileOutputStream
-import java.util.*
+import java.util.Locale
 import javax.inject.Inject
+import timber.log.Timber
 
 /**
  * This activity will set two tabs, achievements and
@@ -45,7 +48,7 @@ class ProfileActivity : BaseActivity() {
     private var contributionsFragment: ContributionsFragment? = null
 
     fun setScroll(canScroll: Boolean) {
-        binding.viewPager.setCanScroll(canScroll)
+        binding.viewPager.canScroll = canScroll
     }
 
     override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -60,6 +63,7 @@ class ProfileActivity : BaseActivity() {
         super.onCreate(savedInstanceState)
 
         binding = ActivityProfileBinding.inflate(layoutInflater)
+        applyEdgeToEdgeAllInsets(binding.root)
         setContentView(binding.root)
         setSupportActionBar(binding.toolbarBinding.toolbar)
 
@@ -71,7 +75,7 @@ class ProfileActivity : BaseActivity() {
         title = userName
         shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false)
 
-        viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
+        viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager)
         binding.viewPager.adapter = viewPagerAdapter
         binding.tabLayout.setupWithViewPager(binding.viewPager)
         setTabs()
@@ -83,39 +87,23 @@ class ProfileActivity : BaseActivity() {
     }
 
     private fun setTabs() {
-        val fragmentList = mutableListOf()
-        val titleList = mutableListOf