mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-11-08 08:01:09 +00:00
Merge c464d60bb156001fbe9c3f8f6c2b1b6f469c263d into 1f24973b06ee77492742ba522002e028f38f4381
This commit is contained in:
commit
62f63d031b
@ -127,6 +127,12 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bitchat" android:host="verify" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bitchat" android:host="geohash" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Persistent foreground service to run the mesh in background -->
|
||||
|
||||
@ -42,6 +42,7 @@ import com.bitchat.android.onboarding.PermissionManager
|
||||
import com.bitchat.android.ui.ChatScreen
|
||||
import com.bitchat.android.ui.ChatViewModel
|
||||
import com.bitchat.android.ui.OrientationAwareActivity
|
||||
import com.bitchat.android.ui.channelForGeohashLink
|
||||
import com.bitchat.android.ui.theme.BitchatTheme
|
||||
import com.bitchat.android.wifiaware.WifiAwareController
|
||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||
@ -705,6 +706,7 @@ class MainActivity : OrientationAwareActivity() {
|
||||
// Handle any notification intent
|
||||
handleNotificationIntent(intent)
|
||||
handleVerificationIntent(intent)
|
||||
handleGeohashLinkIntent(intent)
|
||||
|
||||
// Small delay to ensure mesh service is fully initialized
|
||||
delay(500)
|
||||
@ -734,6 +736,7 @@ class MainActivity : OrientationAwareActivity() {
|
||||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||
handleNotificationIntent(intent)
|
||||
handleVerificationIntent(intent)
|
||||
handleGeohashLinkIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -848,6 +851,16 @@ class MainActivity : OrientationAwareActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGeohashLinkIntent(intent: Intent) {
|
||||
val channel = intent.data?.let(::channelForGeohashLink) ?: return
|
||||
|
||||
// A joined channel stays on screen otherwise, while messages go to the location channel.
|
||||
chatViewModel.exitCurrentChannel()
|
||||
chatViewModel.selectLocationChannel(com.bitchat.android.geohash.ChannelID.Location(channel))
|
||||
chatViewModel.setCurrentGeohash(channel.geohash)
|
||||
chatViewModel.clearNotificationsForGeohash(channel.geohash)
|
||||
}
|
||||
|
||||
private fun handleVerificationIntent(intent: Intent) {
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme != "bitchat" || uri.host != "verify") return
|
||||
|
||||
@ -108,6 +108,11 @@ class ChannelManager(
|
||||
saveChannelData()
|
||||
}
|
||||
|
||||
/** Leaves the joined channel's timeline without changing the location or private chat. */
|
||||
fun exitCurrentChannel() {
|
||||
state.setCurrentChannel(null)
|
||||
}
|
||||
|
||||
fun switchToChannel(channel: String?) {
|
||||
state.setCurrentChannel(channel)
|
||||
state.setSelectedPrivateChatPeer(null)
|
||||
|
||||
@ -684,6 +684,10 @@ class ChatViewModel(
|
||||
channelManager.switchToChannel(channel)
|
||||
}
|
||||
|
||||
fun exitCurrentChannel() {
|
||||
channelManager.exitCurrentChannel()
|
||||
}
|
||||
|
||||
fun leaveChannel(channel: String) {
|
||||
channelManager.leaveChannel(channel)
|
||||
mesh.sendMessage("left $channel", emptyList(), null)
|
||||
|
||||
@ -1170,7 +1170,7 @@ internal fun selectedLocationChannelOutsideNearby(
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateGeohash(geohash: String): Boolean {
|
||||
internal fun validateGeohash(geohash: String): Boolean {
|
||||
if (geohash.isEmpty() || geohash.length > 12) return false
|
||||
val allowed = "0123456789bcdefghjkmnpqrstuvwxyz".toSet()
|
||||
return geohash.all { it in allowed }
|
||||
|
||||
@ -2,6 +2,7 @@ package com.bitchat.android.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.bitchat.android.geohash.GeohashChannel
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
@ -44,6 +45,14 @@ internal fun channelForGeohash(geohash: String): GeohashChannel {
|
||||
return GeohashChannel(level, geohash.lowercase())
|
||||
}
|
||||
|
||||
/** Channel named by a `bitchat://geohash/<geohash>` link, or null if the link is not valid. */
|
||||
internal fun channelForGeohashLink(uri: Uri): GeohashChannel? {
|
||||
if (uri.scheme != "bitchat" || uri.host != "geohash") return null
|
||||
val geohash = uri.path.orEmpty().trim('/').lowercase()
|
||||
if (geohash.length < 2 || !validateGeohash(geohash)) return null
|
||||
return channelForManualGeohash(geohash)
|
||||
}
|
||||
|
||||
internal fun navigateToGeohash(context: Context, geohash: String): Boolean =
|
||||
runCatching {
|
||||
val locationManager = LocationChannelManager.getInstance(context)
|
||||
|
||||
54
app/src/test/java/com/bitchat/android/ui/ChannelExitTest.kt
Normal file
54
app/src/test/java/com/bitchat/android/ui/ChannelExitTest.kt
Normal file
@ -0,0 +1,54 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import com.bitchat.android.geohash.ChannelID
|
||||
import com.bitchat.android.geohash.GeohashChannel
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.mock
|
||||
|
||||
class ChannelExitTest {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val testScope = TestScope(UnconfinedTestDispatcher())
|
||||
private val state = ChatState(scope = testScope)
|
||||
private val channelManager = ChannelManager(
|
||||
state = state,
|
||||
messageManager = MessageManager(state = state),
|
||||
dataManager = mock(),
|
||||
coroutineScope = testScope,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `exiting a joined channel clears it from the timeline`() {
|
||||
state.setCurrentChannel("#test")
|
||||
|
||||
channelManager.exitCurrentChannel()
|
||||
|
||||
assertNull(state.getCurrentChannelValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exiting a joined channel keeps the selected location`() {
|
||||
val location = ChannelID.Location(GeohashChannel(GeohashChannelLevel.CITY, "9q8yy"))
|
||||
state.setCurrentChannel("#test")
|
||||
state.setSelectedLocationChannel(location)
|
||||
|
||||
channelManager.exitCurrentChannel()
|
||||
|
||||
assertEquals(location, state.selectedLocationChannel.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exiting a joined channel keeps the open private chat`() {
|
||||
state.setCurrentChannel("#test")
|
||||
state.setSelectedPrivateChatPeer("peer-a")
|
||||
|
||||
channelManager.exitCurrentChannel()
|
||||
|
||||
assertEquals("peer-a", state.getSelectedPrivateChatPeerValue())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class GeohashLinkRobolectricTest {
|
||||
@Test
|
||||
fun `geohash link selects the channel it names`() {
|
||||
val channel = channelForGeohashLink(Uri.parse("bitchat://geohash/9q8yyk8"))
|
||||
|
||||
assertEquals("9q8yyk8", channel?.geohash)
|
||||
assertEquals(GeohashChannelLevel.BLOCK, channel?.level)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `geohash link level follows geohash length`() {
|
||||
val expectedLevels = mapOf(
|
||||
"9q" to GeohashChannelLevel.REGION,
|
||||
"9q8" to GeohashChannelLevel.PROVINCE,
|
||||
"9q8y" to GeohashChannelLevel.PROVINCE,
|
||||
"9q8yy" to GeohashChannelLevel.CITY,
|
||||
"9q8yyk" to GeohashChannelLevel.NEIGHBORHOOD,
|
||||
"9q8yyk8" to GeohashChannelLevel.BLOCK,
|
||||
"9q8yyk8y" to GeohashChannelLevel.BUILDING,
|
||||
"9q8yyk8ytpxr" to GeohashChannelLevel.BUILDING,
|
||||
)
|
||||
|
||||
expectedLevels.forEach { (geohash, level) ->
|
||||
assertEquals(geohash, level, channelForGeohashLink(Uri.parse("bitchat://geohash/$geohash"))?.level)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `geohash link is normalized like the iOS client`() {
|
||||
assertEquals("9q8yy", channelForGeohashLink(Uri.parse("bitchat://geohash/9Q8YY"))?.geohash)
|
||||
assertEquals("9q8yy", channelForGeohashLink(Uri.parse("bitchat://geohash/9q8yy/"))?.geohash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `malformed geohash links select nothing`() {
|
||||
val rejected = listOf(
|
||||
"bitchat://geohash",
|
||||
"bitchat://geohash/",
|
||||
"bitchat://geohash/9",
|
||||
"bitchat://geohash/9q8yyk8ytpxrz",
|
||||
"bitchat://geohash/9q8ayy",
|
||||
"bitchat://geohash/9q/8y",
|
||||
"bitchat://geohash/9q%238y",
|
||||
"bitchat://geohash/%209q8y",
|
||||
)
|
||||
|
||||
rejected.forEach { link -> assertNull(link, channelForGeohashLink(Uri.parse(link))) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `links for other destinations select nothing`() {
|
||||
assertNull(channelForGeohashLink(Uri.parse("bitchat://verify/9q8yy")))
|
||||
assertNull(channelForGeohashLink(Uri.parse("https://geohash/9q8yy")))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user