diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
index 023ec6b660..cfdc16e06c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
@@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
 import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
 import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
 import org.thoughtcrime.securesms.util.Broadcaster;
+import org.thoughtcrime.securesms.util.VersionUtil;
 import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
 import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
 import org.webrtc.PeerConnectionFactory;
@@ -142,6 +143,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
     private HandlerThread conversationListHandlerThread;
     private Handler conversationListHandler;
     private PersistentLogger persistentLogger;
+    private VersionUtil versionUtil;
 
     @Inject LokiAPIDatabase lokiAPIDatabase;
     @Inject public Storage storage;
@@ -248,6 +250,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
         resubmitProfilePictureIfNeeded();
         loadEmojiSearchIndexIfNeeded();
         EmojiSource.refresh();
+        versionUtil = new VersionUtil(textSecurePreferences);
 
         NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
         HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
@@ -274,6 +277,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
 
             OpenGroupManager.INSTANCE.startPolling();
         });
+
+        // fetch last version data
+        versionUtil.startTimedVersionCheck();
     }
 
     @Override
@@ -286,12 +292,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
             poller.stopIfNeeded();
         }
         ClosedGroupPollerV2.getShared().stopAll();
+        versionUtil.stopTimedVersionCheck();
     }
 
     @Override
     public void onTerminate() {
         stopKovenant(); // Loki
         OpenGroupManager.INSTANCE.stopPolling();
+        versionUtil.clear();
         super.onTerminate();
     }
 
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
index f294e387ff..071da43311 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
@@ -18,7 +18,7 @@ fun showMuteDialog(
 
 private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
     ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
-    TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
+    TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)),
     ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
     SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
     FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java
index 4ee43e1e9c..f193827efd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java
@@ -22,7 +22,7 @@ public class MemoryFileUtil {
 
       int fd = field.getInt(fileDescriptor);
 
-      return ParcelFileDescriptor.adoptFd(fd);
+      return ParcelFileDescriptor.fromFd(fd);
     } catch (IllegalAccessException e) {
       throw new IOException(e);
     } catch (InvocationTargetException e) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt
new file mode 100644
index 0000000000..76075b4ac1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt
@@ -0,0 +1,68 @@
+package org.thoughtcrime.securesms.util
+
+import android.os.Handler
+import android.os.Looper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.session.libsession.messaging.file_server.FileServerApi
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsignal.utilities.Log
+import java.util.concurrent.TimeUnit
+
+class VersionUtil(
+    private val prefs: TextSecurePreferences
+) {
+    private val TAG: String = VersionUtil::class.java.simpleName
+    private val FOUR_HOURS: Long = TimeUnit.HOURS.toMillis(4)
+
+    private val handler = Handler(Looper.getMainLooper())
+    private val runnable: Runnable
+
+    private val scope = CoroutineScope(Dispatchers.Default)
+    private var job: Job? = null
+
+    init {
+        runnable = Runnable {
+            fetchAndScheduleNextVersionCheck()
+        }
+    }
+
+    fun startTimedVersionCheck() {
+        handler.post(runnable)
+    }
+
+    fun stopTimedVersionCheck() {
+        handler.removeCallbacks(runnable)
+    }
+
+    fun clear() {
+        job?.cancel()
+        stopTimedVersionCheck()
+    }
+
+    private fun fetchAndScheduleNextVersionCheck() {
+        fetchVersionData()
+        handler.postDelayed(runnable, FOUR_HOURS)
+    }
+
+    private fun fetchVersionData() {
+        // only perform this if at least 4h has elapsed since th last successful check
+        val lastCheck = System.currentTimeMillis() - prefs.getLastVersionCheck()
+        if (lastCheck < FOUR_HOURS) return
+
+        job?.cancel()
+        job = scope.launch {
+            try {
+                // perform the version check
+                val clientVersion = FileServerApi.getClientVersion()
+                Log.i(TAG, "Fetched version data: $clientVersion")
+                prefs.setLastVersionCheck()
+            } catch (e: Exception) {
+                // we can silently ignore the error
+                Log.e(TAG, "Error fetching version data: $e")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/cpp/CMakeLists.txt b/libsession-util/src/main/cpp/CMakeLists.txt
index 47fee4803c..f65667bcb5 100644
--- a/libsession-util/src/main/cpp/CMakeLists.txt
+++ b/libsession-util/src/main/cpp/CMakeLists.txt
@@ -30,6 +30,7 @@ set(SOURCES
         config_base.cpp
         contacts.cpp
         conversation.cpp
+        blinded_key.cpp
         util.cpp)
 
 add_library( # Sets the name of the library.
diff --git a/libsession-util/src/main/cpp/blinded_key.cpp b/libsession-util/src/main/cpp/blinded_key.cpp
new file mode 100644
index 0000000000..1ed1cfd554
--- /dev/null
+++ b/libsession-util/src/main/cpp/blinded_key.cpp
@@ -0,0 +1,34 @@
+#include <jni.h>
+#include <session/blinding.hpp>
+
+#include "util.h"
+#include "jni_utils.h"
+
+//
+// Created by Thomas Ruffie on 29/7/2024.
+//
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionKeyPair(JNIEnv *env,
+                                                                                  jobject thiz,
+                                                                                  jbyteArray ed25519_secret_key) {
+    return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
+        const auto [pk, sk] = session::blind_version_key_pair(util::ustring_from_bytes(env, ed25519_secret_key));
+
+        jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair");
+        jmethodID kp_constructor = env->GetMethodID(kp_class, "<init>", "([B[B)V");
+        return env->NewObject(kp_class, kp_constructor, util::bytes_from_ustring(env, {pk.data(), pk.size()}), util::bytes_from_ustring(env, {sk.data(), sk.size()}));
+    });
+}
+extern "C"
+JNIEXPORT jbyteArray JNICALL
+Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionSign(JNIEnv *env,
+                                                                               jobject thiz,
+                                                                               jbyteArray ed25519_secret_key,
+                                                                               jlong timestamp) {
+    return jni_utils::run_catching_cxx_exception_or_throws<jbyteArray>(env, [=] {
+        auto bytes = session::blind_version_sign(util::ustring_from_bytes(env, ed25519_secret_key), session::Platform::android, timestamp);
+        return util::bytes_from_ustring(env, bytes);
+    });
+}
\ No newline at end of file
diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt
new file mode 100644
index 0000000000..cd3dac3af2
--- /dev/null
+++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt
@@ -0,0 +1,15 @@
+package network.loki.messenger.libsession_util.util
+
+object BlindKeyAPI {
+    private val loadLibrary by lazy {
+        System.loadLibrary("session_util")
+    }
+
+    init {
+        // Ensure the library is loaded at initialization
+        loadLibrary
+    }
+
+    external fun blindVersionKeyPair(ed25519SecretKey: ByteArray): KeyPair
+    external fun blindVersionSign(ed25519SecretKey: ByteArray, timestamp: Long): ByteArray
+}
\ No newline at end of file
diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt
index 5bffed57ee..fb9c014a15 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt
@@ -1,18 +1,22 @@
 package org.session.libsession.messaging.file_server
 
+import android.util.Base64
+import network.loki.messenger.libsession_util.util.BlindKeyAPI
 import nl.komponents.kovenant.Promise
 import nl.komponents.kovenant.functional.map
-import okhttp3.Headers
 import okhttp3.Headers.Companion.toHeaders
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import okhttp3.MediaType
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.RequestBody
+import org.session.libsession.messaging.MessagingModuleConfiguration
 import org.session.libsession.snode.OnionRequestAPI
+import org.session.libsession.snode.utilities.await
 import org.session.libsignal.utilities.HTTP
 import org.session.libsignal.utilities.JsonUtil
 import org.session.libsignal.utilities.Log
+import org.session.libsignal.utilities.toHexString
+import java.util.concurrent.TimeUnit
 
 object FileServerApi {
 
@@ -23,6 +27,7 @@ object FileServerApi {
     sealed class Error(message: String) : Exception(message) {
         object ParsingFailed : Error("Invalid response.")
         object InvalidURL : Error("Invalid URL.")
+        object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
     }
 
     data class Request(
@@ -105,4 +110,53 @@ object FileServerApi {
         val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
         return send(request)
     }
+
+    /**
+     * Returns the current version of session
+     * This is effectively proxying (and caching) the response from the github release
+     * page.
+     *
+     * Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24
+     * hours out of date if we cannot reach the Github API for some reason.
+     *
+     * https://github.com/oxen-io/session-file-server/blob/dev/doc/api.yaml#L119
+     */
+    suspend fun getClientVersion(): VersionData {
+        // Generate the auth signature
+        val secretKey =  MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes
+            ?: throw (Error.NoEd25519KeyPair)
+
+        val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey)
+        val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) //  The current timestamp in seconds
+        val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp)
+
+        // The hex encoded version-blinded public key with a 07 prefix
+        val blindedPkHex = buildString {
+            append("07")
+            append(blindedKeys.pubKey.toHexString())
+        }
+
+        val request = Request(
+            verb = HTTP.Verb.GET,
+            endpoint = "session_version",
+            queryParameters = mapOf("platform" to "android"),
+            headers = mapOf(
+                "X-FS-Pubkey" to blindedPkHex,
+                "X-FS-Timestamp" to timestamp.toString(),
+                "X-FS-Signature" to Base64.encodeToString(signature, Base64.NO_WRAP)
+            )
+        )
+
+        // transform the promise into a coroutine
+        val result = send(request).await()
+
+        // map out the result
+        return JsonUtil.fromJson(result, Map::class.java).let {
+            VersionData(
+                statusCode = it["status_code"] as? Int ?: 0,
+                version = it["result"] as? String ?: "",
+                updated = it["updated"] as? Double ?: 0.0
+            )
+        }
+    }
 }
\ No newline at end of file
diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt
new file mode 100644
index 0000000000..b7c28020e7
--- /dev/null
+++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt
@@ -0,0 +1,7 @@
+package org.session.libsession.messaging.file_server
+
+data class VersionData(
+    val statusCode: Int, // The value 200. Included for backwards compatibility, and may be removed someday.
+    val version: String, // The Session version.
+    val updated: Double // The unix timestamp when this version was retrieved from Github; this can be up to 24 hours ago in case of consistent fetch errors, though normally will be within the last 30 minutes.
+)
\ No newline at end of file
diff --git a/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt
new file mode 100644
index 0000000000..9c55a2282a
--- /dev/null
+++ b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt
@@ -0,0 +1,13 @@
+package org.session.libsession.snode.utilities
+
+import nl.komponents.kovenant.Promise
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+suspend fun <T, E: Throwable> Promise<T, E>.await(): T {
+    return suspendCoroutine { cont ->
+        success(cont::resume)
+        fail(cont::resumeWithException)
+    }
+}
\ No newline at end of file
diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt
index 12fdd4ceaf..5bf109843d 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt
+++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import org.session.libsession.R
+import org.session.libsession.utilities.TextSecurePreferences.Companion
 import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES
 import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED
 import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK
@@ -20,6 +21,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_
 import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS
 import org.session.libsession.utilities.TextSecurePreferences.Companion.HIDE_PASSWORD
 import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME
+import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VERSION_CHECK
 import org.session.libsession.utilities.TextSecurePreferences.Companion.LEGACY_PREF_KEY_SELECTED_UI_MODE
 import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK
 import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT
@@ -186,6 +188,8 @@ interface TextSecurePreferences {
     fun clearAll()
     fun getHidePassword(): Boolean
     fun setHidePassword(value: Boolean)
+    fun getLastVersionCheck(): Long
+    fun setLastVersionCheck()
 
     companion object {
         val TAG = TextSecurePreferences::class.simpleName
@@ -272,6 +276,7 @@ interface TextSecurePreferences {
         const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
         const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
         const val SELECTED_ACCENT_COLOR = "selected_accent_color"
+        const val LAST_VERSION_CHECK = "pref_last_version_check"
 
         const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config"
         const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config"
@@ -1541,6 +1546,14 @@ class AppTextSecurePreferences @Inject constructor(
         setLongPreference(LAST_VACUUM_TIME, System.currentTimeMillis())
     }
 
+    override fun getLastVersionCheck(): Long {
+        return getLongPreference(LAST_VERSION_CHECK, 0)
+    }
+
+    override fun setLastVersionCheck() {
+        setLongPreference(LAST_VERSION_CHECK, System.currentTimeMillis())
+    }
+
     override fun setShownCallNotification(): Boolean {
         val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false)
         if (previousValue) return false