Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9e25ee4
Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025
actions-user Sep 21, 2025
6cccaae
Merge remote-tracking branch 'origin/main'
yet300 Sep 24, 2025
95358ac
Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025
actions-user Sep 28, 2025
696f698
refactor: new close button like ios(but not liquid glass)
yet300 Sep 30, 2025
e0c7240
Merge remote-tracking branch 'origin/main'
yet300 Sep 30, 2025
16d67d3
feat: Migrate from Parcelable to Kotlinx Serialization
yet300 Sep 30, 2025
ee08835
Merge branch 'permissionlesstech:main' into main
yet300 Sep 30, 2025
95e08d4
feat: Add JsonUtil to replace Gson
yet300 Sep 30, 2025
045db11
feat: Migrate from Gson to Kotlinx Serialization
yet300 Sep 30, 2025
866b8da
Refactor: Migrate Nostr JSON handling from Gson to kotlinx.serialization
yet300 Sep 30, 2025
d54fc17
Refactor: Use kotlinx.serialization for NoisePayload
yet300 Sep 30, 2025
d49ea7f
Refactor: Replace Gson with Kotlinx Serialization
yet300 Sep 30, 2025
af46f69
Refactor: Remove Gson dependency
yet300 Sep 30, 2025
148f804
Merge branch 'main' into feature/migrate-kotlinx.serialization
yet300 Oct 5, 2025
74d90fd
Merge branch 'main' into feature/migrate-kotlinx.serialization
yet300 Oct 13, 2025
314b8ea
Merge remote-tracking branch 'upstream/main' into feature/migrate-kot…
yet300 Oct 24, 2025
df5375f
Refactor: Replace CloseButton with TextButton in bottom sheets
yet300 Oct 25, 2025
4e8c69e
Fix serialization of Any types in NostrEvent, LocationChannelManager,…
yet300 Nov 24, 2025
efa5e9f
fix: Improve geohash channel deserialization
yet300 Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose)
}

Expand Down Expand Up @@ -82,13 +82,13 @@ dependencies {

// Cryptography
implementation(libs.bundles.cryptography)

// JSON
implementation(libs.gson)


// Coroutines
implementation(libs.kotlinx.coroutines.android)

// Serialization
implementation(libs.kotlinx.serialization.json)

// Bluetooth
implementation(libs.nordic.ble)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package com.bitchat.android.favorites
import android.content.Context
import android.util.Log
import com.bitchat.android.identity.SecureIdentityStateManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import com.bitchat.android.util.JsonUtil
import java.util.*

/**
Expand Down Expand Up @@ -82,7 +84,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
}

private val stateManager = SecureIdentityStateManager(context)
private val gson = Gson()

private val favorites = mutableMapOf<String, FavoriteRelationship>() // noiseHex -> relationship
// NEW: Index by current mesh peerID (16-hex) for direct lookup when sending Nostr DMs from mesh context
private val peerIdIndex = mutableMapOf<String, String>() // peerID (lowercase 16-hex) -> npub
Expand Down Expand Up @@ -258,14 +260,14 @@ class FavoritesPersistenceService private constructor(private val context: Conte
try {
val favoritesJson = stateManager.getSecureValue(FAVORITES_KEY)
if (favoritesJson != null) {
val type = object : TypeToken<Map<String, FavoriteRelationshipData>>() {}.type
val data: Map<String, FavoriteRelationshipData> = gson.fromJson(favoritesJson, type)

favorites.clear()
data.forEach { (key, relationshipData) ->
favorites[key] = relationshipData.toFavoriteRelationship()
val data = JsonUtil.fromJsonOrNull(MapSerializer(String.serializer(), FavoriteRelationshipData.serializer()), favoritesJson)
if (data != null) {
favorites.clear()
data.forEach { (key, relationshipData) ->
favorites[key] = relationshipData.toFavoriteRelationship()
}
Log.d(TAG, "Loaded ${favorites.size} favorite relationships")
}
Log.d(TAG, "Loaded ${favorites.size} favorite relationships")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load favorites: ${e.message}")
Expand All @@ -277,7 +279,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
val data = favorites.mapValues { (_, relationship) ->
FavoriteRelationshipData.fromFavoriteRelationship(relationship)
}
val favoritesJson = gson.toJson(data)
val favoritesJson = JsonUtil.toJson(MapSerializer(String.serializer(), FavoriteRelationshipData.serializer()), data)
stateManager.storeSecureValue(FAVORITES_KEY, favoritesJson)
Log.d(TAG, "Saved ${favorites.size} favorite relationships")
} catch (e: Exception) {
Expand All @@ -289,10 +291,11 @@ class FavoritesPersistenceService private constructor(private val context: Conte
try {
val json = stateManager.getSecureValue(PEERID_INDEX_KEY)
if (json != null) {
val type = object : TypeToken<Map<String, String>>() {}.type
val data: Map<String, String> = gson.fromJson(json, type)
peerIdIndex.clear()
peerIdIndex.putAll(data)
val data = JsonUtil.fromJsonOrNull(MapSerializer(String.serializer(), String.serializer()), json)
if (data != null) {
peerIdIndex.clear()
peerIdIndex.putAll(data)
}
Log.d(TAG, "Loaded ${peerIdIndex.size} peerID→npub mappings")
}
} catch (e: Exception) {
Expand All @@ -302,7 +305,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte

private fun savePeerIdIndex() {
try {
val json = gson.toJson(peerIdIndex)
val json = JsonUtil.toJson(MapSerializer(String.serializer(), String.serializer()), peerIdIndex)
stateManager.storeSecureValue(PEERID_INDEX_KEY, json)
Log.d(TAG, "Saved ${peerIdIndex.size} peerID→npub mappings")
} catch (e: Exception) {
Expand Down Expand Up @@ -336,6 +339,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
}

/** Serializable data for JSON storage */
@Serializable
private data class FavoriteRelationshipData(
val peerNoisePublicKeyHex: String,
val peerNostrPublicKey: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import android.location.LocationManager
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import com.bitchat.android.util.JsonUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -41,7 +43,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
}
}

private val gson = Gson()

private val prefs = context.getSharedPreferences("geohash_prefs", Context.MODE_PRIVATE)

private val membership = mutableSetOf<String>()
Expand Down Expand Up @@ -96,29 +98,31 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
try {
val arrJson = prefs.getString(STORE_KEY, null)
if (!arrJson.isNullOrEmpty()) {
val listType = object : TypeToken<List<String>>() {}.type
val arr = gson.fromJson<List<String>>(arrJson, listType)
val seen = mutableSetOf<String>()
val ordered = mutableListOf<String>()
arr.forEach { raw ->
val arr = JsonUtil.fromJsonOrNull(ListSerializer(String.serializer()), arrJson)
if (arr != null) {
val seen = mutableSetOf<String>()
val ordered = mutableListOf<String>()
arr.forEach { raw ->
val gh = normalize(raw)
if (gh.isNotEmpty() && !seen.contains(gh)) {
seen.add(gh)
ordered.add(gh)
}
}
membership.clear(); membership.addAll(seen)
_bookmarks.postValue(ordered)
}
membership.clear(); membership.addAll(seen)
_bookmarks.postValue(ordered)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load bookmarks: ${e.message}")
}
try {
val namesJson = prefs.getString(NAMES_STORE_KEY, null)
if (!namesJson.isNullOrEmpty()) {
val mapType = object : TypeToken<Map<String, String>>() {}.type
val dict = gson.fromJson<Map<String, String>>(namesJson, mapType)
_bookmarkNames.postValue(dict)
val dict = JsonUtil.fromJsonOrNull(MapSerializer(String.serializer(), String.serializer()), namesJson)
if (dict != null) {
_bookmarkNames.postValue(dict)
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load bookmark names: ${e.message}")
Expand All @@ -127,14 +131,14 @@ class GeohashBookmarksStore private constructor(private val context: Context) {

private fun persist() {
try {
val json = gson.toJson(_bookmarks.value ?: emptyList<String>())
val json = JsonUtil.toJson(ListSerializer(String.serializer()), _bookmarks.value ?: emptyList<String>())
prefs.edit().putString(STORE_KEY, json).apply()
} catch (_: Exception) {}
}

private fun persistNames() {
try {
val json = gson.toJson(_bookmarkNames.value ?: emptyMap<String, String>())
val json = JsonUtil.toJson(MapSerializer(String.serializer(), String.serializer()), _bookmarkNames.value ?: emptyMap<String, String>())
prefs.edit().putString(NAMES_STORE_KEY, json).apply()
} catch (_: Exception) {}
}
Expand Down Expand Up @@ -235,14 +239,14 @@ class GeohashBookmarksStore private constructor(private val context: Context) {

private fun persist(list: List<String>) {
try {
val json = gson.toJson(list)
val json = JsonUtil.toJson(ListSerializer(String.serializer()), list)
prefs.edit().putString(STORE_KEY, json).apply()
} catch (_: Exception) {}
}

private fun persistNames(map: Map<String, String>) {
try {
val json = gson.toJson(map)
val json = JsonUtil.toJson(MapSerializer(String.serializer(), String.serializer()), map)
prefs.edit().putString(NAMES_STORE_KEY, json).apply()
} catch (_: Exception) {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import java.util.*
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import kotlinx.serialization.json.*
import com.bitchat.android.util.JsonUtil

/**
* Manages location permissions, one-shot location retrieval, and computing geohash channels.
Expand Down Expand Up @@ -49,7 +49,7 @@ class LocationChannelManager private constructor(private val context: Context) {
private var lastLocation: Location? = null
private var refreshTimer: Job? = null
private var isGeocoding: Boolean = false
private val gson = Gson()

private var dataManager: com.bitchat.android.ui.DataManager? = null

// Published state for UI bindings (matching iOS @Published properties)
Expand Down Expand Up @@ -544,16 +544,20 @@ class LocationChannelManager private constructor(private val context: Context) {
try {
val channelData = when (channel) {
is ChannelID.Mesh -> {
gson.toJson(mapOf("type" to "mesh"))
val jsonObject = buildJsonObject {
put("type", "mesh")
}
JsonUtil.json.encodeToString(JsonObject.serializer(), jsonObject)
}
is ChannelID.Location -> {
gson.toJson(mapOf(
"type" to "location",
"level" to channel.channel.level.name,
"precision" to channel.channel.level.precision,
"geohash" to channel.channel.geohash,
"displayName" to channel.channel.level.displayName
))
val jsonObject = buildJsonObject {
put("type", "location")
put("level", channel.channel.level.name)
put("precision", channel.channel.level.precision)
put("geohash", channel.channel.geohash)
put("displayName", channel.channel.level.displayName)
}
JsonUtil.json.encodeToString(JsonObject.serializer(), jsonObject)
}
}
dataManager?.saveLastGeohashChannel(channelData)
Expand All @@ -570,15 +574,17 @@ class LocationChannelManager private constructor(private val context: Context) {
try {
val channelData = dataManager?.loadLastGeohashChannel()
if (channelData != null) {
val channelMap = gson.fromJson(channelData, Map::class.java) as? Map<String, Any>
if (channelMap != null) {
val channel = when (channelMap["type"] as? String) {
val channelObject = try {
JsonUtil.json.parseToJsonElement(channelData).jsonObject
} catch (e: Exception) { null }
if (channelObject != null) {
val channel = when (channelObject["type"]?.jsonPrimitive?.contentOrNull) {
"mesh" -> ChannelID.Mesh
"location" -> {
val levelName = channelMap["level"] as? String
val precision = (channelMap["precision"] as? Double)?.toInt()
val geohash = channelMap["geohash"] as? String
val displayName = channelMap["displayName"] as? String
val levelName = channelObject["level"]?.jsonPrimitive?.contentOrNull
val precision = channelObject["precision"]?.jsonPrimitive?.doubleOrNull?.toInt()
val geohash = channelObject["geohash"]?.jsonPrimitive?.contentOrNull
val displayName = channelObject["displayName"]?.jsonPrimitive?.contentOrNull

if (levelName != null && precision != null && geohash != null && displayName != null) {
try {
Expand All @@ -595,7 +601,7 @@ class LocationChannelManager private constructor(private val context: Context) {
}
}
else -> {
Log.w(TAG, "Unknown channel type in persisted data: ${channelMap["type"]}")
Log.w(TAG, "Unknown channel type in persisted data: ${channelObject["type"]}")
null
}
}
Expand All @@ -615,9 +621,6 @@ class LocationChannelManager private constructor(private val context: Context) {
Log.d(TAG, "No persisted channel found, defaulting to Mesh")
_selectedChannel.postValue(ChannelID.Mesh)
}
} catch (e: JsonSyntaxException) {
Log.e(TAG, "Failed to parse persisted channel data: ${e.message}")
_selectedChannel.postValue(ChannelID.Mesh)
} catch (e: Exception) {
Log.e(TAG, "Failed to load persisted channel: ${e.message}")
_selectedChannel.postValue(ChannelID.Mesh)
Expand Down
37 changes: 19 additions & 18 deletions app/src/main/java/com/bitchat/android/model/BitchatMessage.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.bitchat.android.model

import android.os.Parcelable
import com.google.gson.GsonBuilder
import kotlinx.parcelize.Parcelize
import com.bitchat.android.util.DateSerializer
import com.bitchat.android.util.ByteArraySerializer
import kotlinx.serialization.Serializable
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.*

@Parcelize
enum class BitchatMessageType : Parcelable {
@Serializable
enum class BitchatMessageType {
Message,
Audio,
Image,
Expand All @@ -18,23 +18,24 @@ enum class BitchatMessageType : Parcelable {
/**
* Delivery status for messages - exact same as iOS version
*/
sealed class DeliveryStatus : Parcelable {
@Parcelize
@Serializable
sealed class DeliveryStatus {
@Serializable
object Sending : DeliveryStatus()

@Parcelize
@Serializable
object Sent : DeliveryStatus()

@Parcelize
data class Delivered(val to: String, val at: Date) : DeliveryStatus()
@Serializable
data class Delivered(val to: String, @Serializable(with = DateSerializer::class) val at: Date) : DeliveryStatus()

@Parcelize
data class Read(val by: String, val at: Date) : DeliveryStatus()
@Serializable
data class Read(val by: String, @Serializable(with = DateSerializer::class) val at: Date) : DeliveryStatus()

@Parcelize
@Serializable
data class Failed(val reason: String) : DeliveryStatus()

@Parcelize
@Serializable
data class PartiallyDelivered(val reached: Int, val total: Int) : DeliveryStatus()

fun getDisplayText(): String {
Expand All @@ -52,25 +53,25 @@ sealed class DeliveryStatus : Parcelable {
/**
* BitchatMessage - 100% compatible with iOS version
*/
@Parcelize
@Serializable
data class BitchatMessage(
val id: String = UUID.randomUUID().toString().uppercase(),
val sender: String,
val content: String,
val type: BitchatMessageType = BitchatMessageType.Message,
val timestamp: Date,
@Serializable(with = DateSerializer::class) val timestamp: Date,
val isRelay: Boolean = false,
val originalSender: String? = null,
val isPrivate: Boolean = false,
val recipientNickname: String? = null,
val senderPeerID: String? = null,
val mentions: List<String>? = null,
val channel: String? = null,
val encryptedContent: ByteArray? = null,
@Serializable(with = ByteArraySerializer::class) val encryptedContent: ByteArray? = null,
val isEncrypted: Boolean = false,
val deliveryStatus: DeliveryStatus? = null,
val powDifficulty: Int? = null
) : Parcelable {
) {

/**
* Convert message to binary payload format - exactly same as iOS version
Expand Down
Loading
Loading