mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-10-25 07:40:36 +00:00
added NIP-09 event deletion request for location note
This commit is contained in:
parent
b7f0b33d3a
commit
817f86b440
@ -67,6 +67,9 @@ class LocationNotesManager private constructor() {
|
||||
// Published state (StateFlow for Android)
|
||||
private val _notes = MutableStateFlow<List<Note>>(emptyList())
|
||||
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
|
||||
|
||||
private val _localPubkey = MutableStateFlow<String?>(null)
|
||||
val localPubkey: StateFlow<String?> = _localPubkey.asStateFlow()
|
||||
|
||||
private val _geohash = MutableStateFlow<String?>(null)
|
||||
val geohash: StateFlow<String?> = _geohash.asStateFlow()
|
||||
@ -145,6 +148,14 @@ class LocationNotesManager private constructor() {
|
||||
noteIDs.clear()
|
||||
_geohash.value = normalized
|
||||
|
||||
// Derive and cache local pubkey for ownership checks
|
||||
scope.launch {
|
||||
runCatching {
|
||||
val identity = withContext(Dispatchers.IO) { deriveIdentityFunc?.invoke(normalized) }
|
||||
if (identity != null) _localPubkey.value = identity.publicKeyHex
|
||||
}
|
||||
}
|
||||
|
||||
// Compute target geohashes: center + neighbors (±1)
|
||||
val neighbors = try {
|
||||
com.bitchat.android.geohash.Geohash.neighborsSamePrecision(normalized)
|
||||
@ -285,6 +296,54 @@ class LocationNotesManager private constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a location note by sending a NIP-09 kind:5 deletion event to relays.
|
||||
* Removes the note locally immediately for optimistic UI, then broadcasts the
|
||||
* deletion event. Only notes authored by the local user can be deleted.
|
||||
*/
|
||||
fun deleteNote(noteId: String) {
|
||||
val currentGeohash = _geohash.value ?: run {
|
||||
Log.w(TAG, "Cannot delete note - no geohash set")
|
||||
return
|
||||
}
|
||||
val deriveIdentity = deriveIdentityFunc ?: run {
|
||||
Log.e(TAG, "Cannot delete note - deriveIdentity not initialized")
|
||||
return
|
||||
}
|
||||
val relays = try {
|
||||
com.bitchat.android.nostr.RelayDirectory.closestRelaysForGeohash(currentGeohash, 5)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to lookup relays for geohash $currentGeohash: ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val identity = withContext(Dispatchers.IO) { deriveIdentity(currentGeohash) }
|
||||
|
||||
val deletionEvent = withContext(Dispatchers.IO) {
|
||||
NostrProtocol.createDeletionEvent(
|
||||
targetEventId = noteId,
|
||||
senderIdentity = identity
|
||||
)
|
||||
}
|
||||
|
||||
// Optimistic local removal
|
||||
_notes.value = _notes.value.filter { it.id != noteId }
|
||||
noteIDs.remove(noteId)
|
||||
|
||||
// Broadcast to geo relays
|
||||
withContext(Dispatchers.IO) {
|
||||
sendEventFunc?.invoke(deletionEvent, relays.ifEmpty { null })
|
||||
}
|
||||
|
||||
Log.d(TAG, "✅ Note deleted: ${noteId.take(16)}...")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to delete note: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to location notes for current geohash
|
||||
*/
|
||||
@ -326,9 +385,11 @@ class LocationNotesManager private constructor() {
|
||||
|
||||
// Subscribe for each geohash in the ±1 set
|
||||
subscribedGeohashes.forEach { gh ->
|
||||
val filter = NostrFilter.geohashNotes(
|
||||
geohash = gh,
|
||||
since = null,
|
||||
// Include kind:5 (NIP-09 deletion) alongside kind:1 notes so remote
|
||||
// deletions are reflected in real time for all subscribers
|
||||
val filter = NostrFilter(
|
||||
kinds = listOf(NostrKind.TEXT_NOTE, NostrKind.DELETION),
|
||||
tagFilters = mapOf("g" to listOf(gh)),
|
||||
limit = 200
|
||||
)
|
||||
val subId = "location-notes-$gh"
|
||||
@ -356,7 +417,24 @@ class LocationNotesManager private constructor() {
|
||||
* Handle incoming event from subscription
|
||||
*/
|
||||
private fun handleEvent(event: NostrEvent) {
|
||||
// Validate event
|
||||
// Handle NIP-09 deletion events: remove notes authored by the sender
|
||||
if (event.kind == NostrKind.DELETION) {
|
||||
val targetIds = event.tags
|
||||
.filter { it.size >= 2 && it[0] == "e" }
|
||||
.map { it[1] }.toSet()
|
||||
if (targetIds.isNotEmpty()) {
|
||||
val before = _notes.value
|
||||
val removed = before.filter { it.id in targetIds && it.pubkey == event.pubkey }
|
||||
if (removed.isNotEmpty()) {
|
||||
_notes.value = before.filter { it.id !in targetIds || it.pubkey != event.pubkey }
|
||||
removed.forEach { noteIDs.remove(it.id) }
|
||||
Log.d(TAG, "🗑️ Removed ${removed.size} note(s) via kind:5 from ${event.pubkey.take(8)}")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Validate event — only TEXT_NOTE beyond this point
|
||||
if (event.kind != NostrKind.TEXT_NOTE) {
|
||||
Log.v(TAG, "Ignoring non-text-note event: kind=${event.kind}")
|
||||
return
|
||||
|
||||
@ -210,6 +210,7 @@ data class NostrEvent(
|
||||
object NostrKind {
|
||||
const val METADATA = 0
|
||||
const val TEXT_NOTE = 1
|
||||
const val DELETION = 5 // NIP-09 event deletion request
|
||||
const val DIRECT_MESSAGE = 14 // NIP-17 direct message (unsigned)
|
||||
const val FILE_MESSAGE = 15 // NIP-17 file message (unsigned)
|
||||
const val SEAL = 13 // NIP-17 sealed event
|
||||
|
||||
@ -128,6 +128,24 @@ object NostrProtocol {
|
||||
return@withContext senderIdentity.signEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a NIP-09 deletion event (kind 5) for a given note
|
||||
* Signals to relays and other clients that the note should be removed
|
||||
*/
|
||||
suspend fun createDeletionEvent(
|
||||
targetEventId: String,
|
||||
senderIdentity: NostrIdentity
|
||||
): NostrEvent = withContext(Dispatchers.Default) {
|
||||
val event = NostrEvent(
|
||||
pubkey = senderIdentity.publicKeyHex,
|
||||
createdAt = (System.currentTimeMillis() / 1000).toInt(),
|
||||
kind = NostrKind.DELETION,
|
||||
tags = listOf(listOf("e", targetEventId)),
|
||||
content = ""
|
||||
)
|
||||
return@withContext senderIdentity.signEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a geohash-scoped presence event (kind 20001)
|
||||
* Has no content and no nickname, used for participant counting
|
||||
|
||||
@ -3,6 +3,7 @@ package com.bitchat.android.ui
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@ -17,21 +18,25 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import com.bitchat.android.R
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.nostr.LocationNotesManager
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
@ -47,36 +52,40 @@ fun LocationNotesSheet(
|
||||
locationName: String?,
|
||||
nickname: String?,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
onNoteLongClick: (LocationNotesManager.Note) -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
||||
|
||||
// iOS color scheme
|
||||
val accentGreen = if (isDark) Color.Green else Color(0xFF008000) // dark: green, light: dark green (0, 0.5, 0)
|
||||
|
||||
val accentGreen =
|
||||
if (isDark) Color.Green else Color(0xFF008000) // dark: green, light: dark green (0, 0.5, 0)
|
||||
|
||||
// Managers
|
||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
|
||||
|
||||
// State
|
||||
val notes by notesManager.notes.collectAsStateWithLifecycle()
|
||||
val state by notesManager.state.collectAsStateWithLifecycle(LocationNotesManager.State.IDLE)
|
||||
val errorMessage by notesManager.errorMessage.collectAsStateWithLifecycle()
|
||||
val initialLoadComplete by notesManager.initialLoadComplete.collectAsStateWithLifecycle(false)
|
||||
|
||||
|
||||
// SIMPLIFIED: Get count directly from notes list (no separate counter needed)
|
||||
val count = notes.size
|
||||
|
||||
|
||||
// Get location name (building or block) - matches iOS locationNames lookup
|
||||
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||
val displayLocationName = locationNames[GeohashChannelLevel.BUILDING]?.takeIf { it.isNotEmpty() }
|
||||
?: locationNames[GeohashChannelLevel.BLOCK]?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val displayLocationName =
|
||||
locationNames[GeohashChannelLevel.BUILDING]?.takeIf { it.isNotEmpty() }
|
||||
?: locationNames[GeohashChannelLevel.BLOCK]?.takeIf { it.isNotEmpty() }
|
||||
|
||||
// Input field state
|
||||
var draft by remember { mutableStateOf("") }
|
||||
val sendButtonEnabled = draft.trim().isNotEmpty() && state != LocationNotesManager.State.NO_RELAYS
|
||||
|
||||
val sendButtonEnabled =
|
||||
draft.trim().isNotEmpty() && state != LocationNotesManager.State.NO_RELAYS
|
||||
|
||||
// Scroll state
|
||||
val listState = rememberLazyListState()
|
||||
val isScrolled by remember {
|
||||
@ -86,7 +95,7 @@ fun LocationNotesSheet(
|
||||
}
|
||||
val topBarAlpha by animateFloatAsState(
|
||||
targetValue = if (isScrolled) 0.95f else 0f,
|
||||
label = "topBarAlpha"
|
||||
label = "topBarAlpha",
|
||||
)
|
||||
|
||||
// Refresh location when sheet opens
|
||||
@ -98,7 +107,7 @@ fun LocationNotesSheet(
|
||||
LaunchedEffect(geohash) {
|
||||
notesManager.setGeohash(geohash)
|
||||
}
|
||||
|
||||
|
||||
// Cleanup when sheet closes
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
@ -113,8 +122,11 @@ fun LocationNotesSheet(
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
contentPadding = PaddingValues(top = 64.dp, bottom = 20.dp)
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
contentPadding = PaddingValues(top = 64.dp, bottom = 20.dp),
|
||||
) {
|
||||
item(key = "notes_header") {
|
||||
LocationNotesHeader(
|
||||
@ -129,23 +141,29 @@ fun LocationNotesSheet(
|
||||
state == LocationNotesManager.State.NO_RELAYS -> {
|
||||
item {
|
||||
NoRelaysRow(
|
||||
onRetry = { notesManager.refresh() }
|
||||
onRetry = { notesManager.refresh() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state == LocationNotesManager.State.LOADING && !initialLoadComplete -> {
|
||||
item {
|
||||
LoadingRow()
|
||||
}
|
||||
}
|
||||
|
||||
notes.isEmpty() -> {
|
||||
item {
|
||||
EmptyRow()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
items(notes, key = { it.id }) { note ->
|
||||
NoteRow(note = note)
|
||||
NoteRow(
|
||||
note = note,
|
||||
onLongClick = onNoteLongClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
item {
|
||||
@ -160,7 +178,7 @@ fun LocationNotesSheet(
|
||||
item {
|
||||
ErrorRow(
|
||||
message = error,
|
||||
onDismiss = { notesManager.clearError() }
|
||||
onDismiss = { notesManager.clearError() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -173,27 +191,29 @@ fun LocationNotesSheet(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
title = {
|
||||
BitchatSheetTitle(
|
||||
text = pluralStringResource(
|
||||
id = R.plurals.location_notes_title,
|
||||
count = count,
|
||||
geohash,
|
||||
count
|
||||
)
|
||||
text =
|
||||
pluralStringResource(
|
||||
id = R.plurals.location_notes_title,
|
||||
count = count,
|
||||
geohash,
|
||||
count,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
){
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column {
|
||||
// Divider before input (matches iOS overlay)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f),
|
||||
thickness = 1.dp
|
||||
thickness = 1.dp,
|
||||
)
|
||||
|
||||
// Input section (matches iOS inputSection)
|
||||
@ -209,7 +229,7 @@ fun LocationNotesSheet(
|
||||
notesManager.send(content, nickname)
|
||||
draft = ""
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -228,10 +248,11 @@ private fun LocationNotesHeader(
|
||||
accentGreen: Color,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp),
|
||||
) {
|
||||
// Location name in green (building or block)
|
||||
locationName?.let { name ->
|
||||
@ -240,20 +261,20 @@ private fun LocationNotesHeader(
|
||||
text = name,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = accentGreen
|
||||
color = accentGreen,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Description
|
||||
Text(
|
||||
text = stringResource(R.string.location_notes_description),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
)
|
||||
|
||||
|
||||
// Relays paused message if no relays
|
||||
if (state == LocationNotesManager.State.NO_RELAYS) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
@ -261,7 +282,7 @@ private fun LocationNotesHeader(
|
||||
text = stringResource(R.string.location_notes_relays_unavailable),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -272,28 +293,40 @@ private fun LocationNotesHeader(
|
||||
* Shows @basename then timestamp, then content below
|
||||
*/
|
||||
@Composable
|
||||
private fun NoteRow(note: LocationNotesManager.Note) {
|
||||
private fun NoteRow(
|
||||
note: LocationNotesManager.Note,
|
||||
onLongClick: (LocationNotesManager.Note) -> Unit,
|
||||
) {
|
||||
// Extract baseName (before #suffix like iOS)
|
||||
val baseName = note.displayName.split("#", limit = 2).firstOrNull() ?: note.displayName
|
||||
val ts = timestampText(note.createdAt)
|
||||
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.pointerInput(note.id) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLongClick(note)
|
||||
},
|
||||
)
|
||||
}.padding(vertical = 4.dp),
|
||||
) {
|
||||
// First row: @nickname and timestamp
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "@$baseName",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
if (ts.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
@ -301,19 +334,19 @@ private fun NoteRow(note: LocationNotesManager.Note) {
|
||||
text = ts,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
|
||||
// Second row: content
|
||||
Text(
|
||||
text = note.content,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -324,23 +357,24 @@ private fun NoteRow(note: LocationNotesManager.Note) {
|
||||
@Composable
|
||||
private fun NoRelaysRow(onRetry: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.location_notes_no_relays_title),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.location_notes_no_relays_desc),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
@ -348,7 +382,7 @@ private fun NoRelaysRow(onRetry: () -> Unit) {
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.clickable(onClick = onRetry)
|
||||
modifier = Modifier.clickable(onClick = onRetry),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -359,22 +393,23 @@ private fun NoRelaysRow(onRetry: () -> Unit) {
|
||||
@Composable
|
||||
private fun LoadingRow() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.loading_location_notes),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -385,23 +420,24 @@ private fun LoadingRow() {
|
||||
@Composable
|
||||
private fun EmptyRow() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.location_notes_empty_title),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.location_notes_empty_desc),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -410,26 +446,30 @@ private fun EmptyRow() {
|
||||
* Error row - matches iOS errorRow
|
||||
*/
|
||||
@Composable
|
||||
private fun ErrorRow(message: String, onDismiss: () -> Unit) {
|
||||
private fun ErrorRow(
|
||||
message: String,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "⚠",
|
||||
fontSize = 12.sp
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = message,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
@ -438,7 +478,7 @@ private fun ErrorRow(message: String, onDismiss: () -> Unit) {
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.clickable(onClick = onDismiss)
|
||||
modifier = Modifier.clickable(onClick = onDismiss),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -486,68 +526,77 @@ private fun LocationNotesInputSection(
|
||||
) {
|
||||
// Text input with placeholder overlay (matches main chat exactly)
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
androidx.compose.foundation.text.BasicTextField(
|
||||
value = draft,
|
||||
onValueChange = onDraftChange,
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = colorScheme.primary,
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
cursorBrush = androidx.compose.ui.graphics.SolidColor(colorScheme.primary),
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
imeAction = androidx.compose.ui.text.input.ImeAction.Send
|
||||
),
|
||||
keyboardActions = androidx.compose.foundation.text.KeyboardActions(
|
||||
onSend = { if (sendButtonEnabled) onSend() }
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
textStyle =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = colorScheme.primary,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
),
|
||||
cursorBrush =
|
||||
androidx.compose.ui.graphics
|
||||
.SolidColor(colorScheme.primary),
|
||||
keyboardOptions =
|
||||
androidx.compose.foundation.text.KeyboardOptions(
|
||||
imeAction = androidx.compose.ui.text.input.ImeAction.Send,
|
||||
),
|
||||
keyboardActions =
|
||||
androidx.compose.foundation.text.KeyboardActions(
|
||||
onSend = { if (sendButtonEnabled) onSend() },
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
|
||||
// Placeholder when empty (matches main chat)
|
||||
if (draft.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.location_notes_input_placeholder),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
),
|
||||
color = colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Send button - circular with icon (matches main chat exactly)
|
||||
IconButton(
|
||||
onClick = { if (sendButtonEnabled) onSend() },
|
||||
enabled = sendButtonEnabled,
|
||||
modifier = Modifier.size(32.dp)
|
||||
modifier = Modifier.size(32.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.background(
|
||||
color = if (!sendButtonEnabled) {
|
||||
colorScheme.onSurface.copy(alpha = 0.3f)
|
||||
} else {
|
||||
accentGreen.copy(alpha = 0.75f)
|
||||
},
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
modifier =
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.background(
|
||||
color =
|
||||
if (!sendButtonEnabled) {
|
||||
colorScheme.onSurface.copy(alpha = 0.3f)
|
||||
} else {
|
||||
accentGreen.copy(alpha = 0.75f)
|
||||
},
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowUpward,
|
||||
contentDescription = stringResource(R.string.send_message),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = if (!sendButtonEnabled) {
|
||||
colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
} else if (isDark) {
|
||||
Color.Black // Black arrow on green in dark theme
|
||||
} else {
|
||||
Color.White // White arrow on green in light theme
|
||||
}
|
||||
tint =
|
||||
if (!sendButtonEnabled) {
|
||||
colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
} else if (isDark) {
|
||||
Color.Black // Black arrow on green in dark theme
|
||||
} else {
|
||||
Color.White // White arrow on green in light theme
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -555,6 +604,118 @@ private fun LocationNotesInputSection(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note Actions Sheet - shown on long-press of a note
|
||||
* Reuses BitchatBottomSheet pattern from ChatUserSheet
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun NoteActionsSheet(
|
||||
note: LocationNotesManager.Note,
|
||||
onDelete: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isDark =
|
||||
colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||
val standardRed = Color(0xFFFF3B30) // iOS red
|
||||
|
||||
BitchatBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
// Header: note author
|
||||
val baseName = note.displayName.split("#", limit = 2).firstOrNull() ?: note.displayName
|
||||
Text(
|
||||
text = "@$baseName",
|
||||
fontSize = 18.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
// Action list
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
item {
|
||||
NoteActionRow(
|
||||
title = stringResource(R.string.action_delete_note_title),
|
||||
subtitle = stringResource(R.string.action_delete_note_subtitle),
|
||||
titleColor = standardRed,
|
||||
onClick = onDelete,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel button
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.cancel_lower),
|
||||
fontSize = BASE_FONT_SIZE.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single action row inside NoteActionsSheet
|
||||
* Matches UserActionRow pattern from ChatUserSheet
|
||||
*/
|
||||
@Composable
|
||||
private fun NoteActionRow(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
titleColor: Color,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
// iOS-style list row (plain button, no card background)
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
color = Color.Transparent,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = BASE_FONT_SIZE.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = titleColor,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp text - matches iOS timestampText exactly
|
||||
* Shows relative time for < 7 days, absolute date otherwise
|
||||
@ -562,40 +723,47 @@ private fun LocationNotesInputSection(
|
||||
private fun timestampText(createdAt: Int): String {
|
||||
val date = Date(createdAt * 1000L)
|
||||
val now = Date()
|
||||
|
||||
|
||||
// Calculate days difference
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.time = date
|
||||
val dateDay = calendar.get(Calendar.DAY_OF_YEAR)
|
||||
val dateYear = calendar.get(Calendar.YEAR)
|
||||
|
||||
|
||||
calendar.time = now
|
||||
val nowDay = calendar.get(Calendar.DAY_OF_YEAR)
|
||||
val nowYear = calendar.get(Calendar.YEAR)
|
||||
|
||||
val daysDiff = if (dateYear == nowYear) {
|
||||
nowDay - dateDay
|
||||
} else {
|
||||
// Simplified: just check if less than 7 days by timestamp
|
||||
val diff = (now.time - date.time) / (1000 * 60 * 60 * 24)
|
||||
diff.toInt()
|
||||
}
|
||||
|
||||
|
||||
val daysDiff =
|
||||
if (dateYear == nowYear) {
|
||||
nowDay - dateDay
|
||||
} else {
|
||||
// Simplified: just check if less than 7 days by timestamp
|
||||
val diff = (now.time - date.time) / (1000 * 60 * 60 * 24)
|
||||
diff.toInt()
|
||||
}
|
||||
|
||||
return if (daysDiff < 7) {
|
||||
// Relative formatting (abbreviated)
|
||||
val diffMillis = now.time - date.time
|
||||
val diffSeconds = diffMillis / 1000
|
||||
|
||||
|
||||
when {
|
||||
diffSeconds < 60 -> "" // Don't show "just now" in iOS
|
||||
diffSeconds < 60 -> {
|
||||
""
|
||||
}
|
||||
|
||||
// Don't show "just now" in iOS
|
||||
diffSeconds < 3600 -> {
|
||||
val minutes = (diffSeconds / 60).toInt()
|
||||
"${minutes}m ago"
|
||||
}
|
||||
|
||||
diffSeconds < 86400 -> {
|
||||
val hours = (diffSeconds / 3600).toInt()
|
||||
"${hours}h ago"
|
||||
}
|
||||
|
||||
else -> {
|
||||
val days = (diffSeconds / 86400).toInt()
|
||||
"${days}d ago"
|
||||
@ -604,11 +772,12 @@ private fun timestampText(createdAt: Int): String {
|
||||
} else {
|
||||
// Absolute date formatting
|
||||
val sameYear = dateYear == nowYear
|
||||
val formatter = if (sameYear) {
|
||||
SimpleDateFormat("MMM d", Locale.getDefault())
|
||||
} else {
|
||||
SimpleDateFormat("MMM d, y", Locale.getDefault())
|
||||
}
|
||||
val formatter =
|
||||
if (sameYear) {
|
||||
SimpleDateFormat("MMM d", Locale.getDefault())
|
||||
} else {
|
||||
SimpleDateFormat("MMM d, y", Locale.getDefault())
|
||||
}
|
||||
formatter.format(date)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,19 +4,22 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.nostr.LocationNotesManager
|
||||
|
||||
/**
|
||||
* Presenter component for LocationNotesSheet
|
||||
@ -27,29 +30,38 @@ import com.bitchat.android.R
|
||||
@Composable
|
||||
fun LocationNotesSheetPresenter(
|
||||
viewModel: ChatViewModel,
|
||||
onDismiss: () -> Unit
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||
val isLoadingLocation by locationManager.isLoadingLocation.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
|
||||
|
||||
// Note long-press state — owned here so NoteActionsSheet is a sibling of
|
||||
// LocationNotesSheet (not nested inside it), which avoids Dialog window conflicts
|
||||
var selectedNote by remember { mutableStateOf<LocationNotesManager.Note?>(null) }
|
||||
val localPubkey by notesManager.localPubkey.collectAsStateWithLifecycle()
|
||||
|
||||
// iOS pattern: notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
|
||||
val buildingGeohash = availableChannels.firstOrNull { it.level == GeohashChannelLevel.BUILDING }?.geohash
|
||||
|
||||
val buildingGeohash =
|
||||
availableChannels.firstOrNull { it.level == GeohashChannelLevel.BUILDING }?.geohash
|
||||
|
||||
if (buildingGeohash != null) {
|
||||
// Get location name from locationManager
|
||||
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||
val locationName = locationNames[GeohashChannelLevel.BUILDING]
|
||||
?: locationNames[GeohashChannelLevel.BLOCK]
|
||||
|
||||
val locationName =
|
||||
locationNames[GeohashChannelLevel.BUILDING]
|
||||
?: locationNames[GeohashChannelLevel.BLOCK]
|
||||
|
||||
LocationNotesSheet(
|
||||
geohash = buildingGeohash,
|
||||
locationName = locationName,
|
||||
nickname = nickname,
|
||||
onDismiss = onDismiss
|
||||
onDismiss = onDismiss,
|
||||
onNoteLongClick = { note -> selectedNote = note },
|
||||
)
|
||||
} else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && isLoadingLocation) {
|
||||
LocationNotesAcquiringSheet(onDismiss = onDismiss)
|
||||
@ -57,7 +69,19 @@ fun LocationNotesSheetPresenter(
|
||||
// No building geohash available - show error state (matches iOS)
|
||||
LocationNotesErrorSheet(
|
||||
onDismiss = onDismiss,
|
||||
locationManager = locationManager
|
||||
locationManager = locationManager,
|
||||
)
|
||||
}
|
||||
|
||||
// Note actions sheet
|
||||
selectedNote?.takeIf { it.pubkey == localPubkey }?.let { note ->
|
||||
NoteActionsSheet(
|
||||
note = note,
|
||||
onDelete = {
|
||||
notesManager.deleteNote(note.id)
|
||||
selectedNote = null
|
||||
},
|
||||
onDismiss = { selectedNote = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -67,33 +91,32 @@ fun LocationNotesSheetPresenter(
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun LocationNotesAcquiringSheet(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
private fun LocationNotesAcquiringSheet(onDismiss: () -> Unit) {
|
||||
BitchatBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = "Acquiring Location",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(48.dp),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Please wait while your location is being determined",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -106,30 +129,31 @@ private fun LocationNotesAcquiringSheet(
|
||||
@Composable
|
||||
private fun LocationNotesErrorSheet(
|
||||
onDismiss: () -> Unit,
|
||||
locationManager: LocationChannelManager
|
||||
locationManager: LocationChannelManager,
|
||||
) {
|
||||
BitchatBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 80.dp, bottom = 24.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 80.dp, bottom = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "Location Unavailable",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Location permission is required for notes",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(onClick = {
|
||||
@ -148,9 +172,9 @@ private fun LocationNotesErrorSheet(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
title = {
|
||||
BitchatSheetTitle(
|
||||
text = stringResource(R.string.cd_location_notes).uppercase()
|
||||
text = stringResource(R.string.cd_location_notes).uppercase(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">إرسال عناق ودي</string>
|
||||
<string name="action_block_title">حظر %1$s</string>
|
||||
<string name="action_block_subtitle">حظر جميع رسائل هذا المستخدم</string>
|
||||
<string name="action_delete_note_title">حذف الملاحظة</string>
|
||||
<string name="action_delete_note_subtitle">يرسل طلب حذف nostr إلى المرحلات</string>
|
||||
|
||||
<!-- ورقة قنوات geohash -->
|
||||
<string name="location_channels_title">#قنوات الموقع</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">একটি বন্ধুত্বপূর্ণ আলিঙ্গন পাঠান</string>
|
||||
<string name="action_block_title">%1$s কে ব্লক করুন</string>
|
||||
<string name="action_block_subtitle">এই ব্যবহারকারীর সমস্ত বার্তা ব্লক করুন</string>
|
||||
<string name="action_delete_note_title">নোট মুছুন</string>
|
||||
<string name="action_delete_note_subtitle">রিলেতে nostr মুছে ফেলার অনুরোধ পাঠায়</string>
|
||||
|
||||
<!-- জিওহ্যাশ চ্যানেল শিট -->
|
||||
<string name="location_channels_title">#অবস্থান চ্যানেল</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">Eine freundliche Umarmung senden</string>
|
||||
<string name="action_block_title">%1$s blockieren</string>
|
||||
<string name="action_block_subtitle">Alle Nachrichten dieses Benutzers blockieren</string>
|
||||
<string name="action_delete_note_title">Notiz löschen</string>
|
||||
<string name="action_delete_note_subtitle">sendet eine nostr-Löschanfrage an Relays</string>
|
||||
|
||||
<!-- Geohash‑Kanäle Sheet -->
|
||||
<string name="location_channels_title">#Standort‑Kanäle</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">Enviar un abrazo amistoso</string>
|
||||
<string name="action_block_title">Bloquear a %1$s</string>
|
||||
<string name="action_block_subtitle">Bloquear todos los mensajes de este usuario</string>
|
||||
<string name="action_delete_note_title">eliminar nota</string>
|
||||
<string name="action_delete_note_subtitle">envía una solicitud de eliminación de nostr a los relays</string>
|
||||
|
||||
<!-- Hoja de canales geohash -->
|
||||
<string name="location_channels_title">#Canales de ubicación</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">یک بغل دوستانه ارسال کن</string>
|
||||
<string name="action_block_title">%1$s را مسدود کن</string>
|
||||
<string name="action_block_subtitle">همهٔ پیامهای این کاربر را مسدود کن</string>
|
||||
<string name="action_delete_note_title">حذف یادداشت</string>
|
||||
<string name="action_delete_note_subtitle">درخواست حذف nostr را به رلهها ارسال میکند</string>
|
||||
|
||||
<!-- برگهٔ کانالهای geohash -->
|
||||
<string name="location_channels_title">#کانالهای مکانی</string>
|
||||
|
||||
@ -185,6 +185,8 @@
|
||||
<string name="action_hug_subtitle">magpadala ng magiliw na yakap</string>
|
||||
<string name="action_block_title">harangan si %1$s</string>
|
||||
<string name="action_block_subtitle">harangan ang lahat ng mensahe mula sa user na ito</string>
|
||||
<string name="action_delete_note_title">tanggalin ang tala</string>
|
||||
<string name="action_delete_note_subtitle">nagpapadala ng kahilingan sa pagtanggal ng nostr sa mga relay</string>
|
||||
|
||||
<!-- Location channels sheet -->
|
||||
<string name="location_channels_title">#mga channel sa lokasyon</string>
|
||||
|
||||
@ -185,6 +185,8 @@
|
||||
<string name="action_hug_subtitle">envoyer une accolade amicale</string>
|
||||
<string name="action_block_title">bloquer %1$s</string>
|
||||
<string name="action_block_subtitle">bloquer tous les messages de cet utilisateur</string>
|
||||
<string name="action_delete_note_title">supprimer la note</string>
|
||||
<string name="action_delete_note_subtitle">envoie une demande de suppression nostr aux relais</string>
|
||||
|
||||
<!-- Feuille des canaux de lieu -->
|
||||
<string name="location_channels_title">#canaux de lieu</string>
|
||||
|
||||
@ -53,5 +53,7 @@
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="action_delete_note_title">מחק הערה</string>
|
||||
<string name="action_delete_note_subtitle">שולח בקשת מחיקה של nostr לממסרים</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">दोस्ताना आलिंगन भेजें</string>
|
||||
<string name="action_block_title">%1$s को ब्लॉक करें</string>
|
||||
<string name="action_block_subtitle">इस उपयोगकर्ता के सभी संदेश ब्लॉक करें</string>
|
||||
<string name="action_delete_note_title">नोट हटाएं</string>
|
||||
<string name="action_delete_note_subtitle">रिले को nostr हटाने का अनुरोध भेजता है</string>
|
||||
|
||||
<!-- geohash चैनल शीट -->
|
||||
<string name="location_channels_title">#स्थान चैनल</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">Kirim pelukan yang ramah</string>
|
||||
<string name="action_block_title">Blokir %1$s</string>
|
||||
<string name="action_block_subtitle">Blokir semua pesan dari pengguna ini</string>
|
||||
<string name="action_delete_note_title">hapus catatan</string>
|
||||
<string name="action_delete_note_subtitle">mengirim permintaan penghapusan nostr ke relay</string>
|
||||
|
||||
<!-- Sheet channel geohash -->
|
||||
<string name="location_channels_title">#Channel lokasi</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">invia un abbraccio amichevole</string>
|
||||
<string name="action_block_title">blocca %1$s</string>
|
||||
<string name="action_block_subtitle">blocca tutti i messaggi da questo utente</string>
|
||||
<string name="action_delete_note_title">elimina nota</string>
|
||||
<string name="action_delete_note_subtitle">invia una richiesta di eliminazione nostr ai relay</string>
|
||||
|
||||
<!-- Schermata canali di posizione -->
|
||||
<string name="location_channels_title">#canali di posizione</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">フレンドリーなハグを送る</string>
|
||||
<string name="action_block_title">%1$s をブロック</string>
|
||||
<string name="action_block_subtitle">このユーザーからのすべてのメッセージをブロック</string>
|
||||
<string name="action_delete_note_title">ノートを削除</string>
|
||||
<string name="action_delete_note_subtitle">nostr の削除リクエストをリレーに送信します</string>
|
||||
|
||||
<!-- Geohash チャンネル シート -->
|
||||
<string name="location_channels_title">#ロケーション・チャンネル</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">გაგზავნეთ მეგობრული ჩახუტება</string>
|
||||
<string name="action_block_title">%1$s-ის დაბლოკვა</string>
|
||||
<string name="action_block_subtitle">ამ მომხმარებლის ყველა შეტყობინების დაბლოკვა</string>
|
||||
<string name="action_delete_note_title">ჩანაწერის წაშლა</string>
|
||||
<string name="action_delete_note_subtitle">nostr-ის წაშლის მოთხოვნას გზავნის relay-ებზე</string>
|
||||
|
||||
<!-- geohash არხების შიტი -->
|
||||
<string name="location_channels_title">#მდებარეობის არხები</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">친근한 포옹 보내기</string>
|
||||
<string name="action_block_title">%1$s 차단</string>
|
||||
<string name="action_block_subtitle">이 사용자의 모든 메시지 차단</string>
|
||||
<string name="action_delete_note_title">노트 삭제</string>
|
||||
<string name="action_delete_note_subtitle">nostr 삭제 요청을 릴레이에 전송합니다</string>
|
||||
|
||||
<!-- geohash 채널 시트 -->
|
||||
<string name="location_channels_title">#위치 채널</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">handefasa hafatra honofinofy sariaka</string>
|
||||
<string name="action_block_title">hanorina %1$s</string>
|
||||
<string name="action_block_subtitle">hanorina ny hafatra rehetra avy amin\'ity mpampiasa ity</string>
|
||||
<string name="action_delete_note_title">fafao ny fanamarihana</string>
|
||||
<string name="action_delete_note_subtitle">mandefa fangatahana fafana nostr any amin\'ny relay</string>
|
||||
|
||||
<!-- Takelaka fantsona toerana -->
|
||||
<string name="location_channels_title">#fantsona toerana</string>
|
||||
|
||||
@ -40,5 +40,7 @@
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="action_delete_note_title">padam nota</string>
|
||||
<string name="action_delete_note_subtitle">menghantar permintaan pemadaman nostr ke relay</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -185,6 +185,8 @@
|
||||
<string name="action_hug_subtitle">मैत्रीपूर्ण अँगालो पठाउनुहोस्</string>
|
||||
<string name="action_block_title">%1$s लाई ब्लक गर्नुहोस्</string>
|
||||
<string name="action_block_subtitle">यस प्रयोगकर्ताबाट सबै सन्देश ब्लक गर्नुहोस्</string>
|
||||
<string name="action_delete_note_title">नोट मेट्नुहोस्</string>
|
||||
<string name="action_delete_note_subtitle">रिलेहरूमा nostr मेटाउने अनुरोध पठाउँछ</string>
|
||||
|
||||
<!-- स्थान च्यानल शीट -->
|
||||
<string name="location_channels_title">#स्थान च्यानल</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">stuur een vriendelijke knuffel</string>
|
||||
<string name="action_block_title">%1$s blokkeren</string>
|
||||
<string name="action_block_subtitle">blokkeer alle berichten van deze gebruiker</string>
|
||||
<string name="action_delete_note_title">notitie verwijderen</string>
|
||||
<string name="action_delete_note_subtitle">stuurt een nostr-verwijderverzoek naar relays</string>
|
||||
|
||||
<!-- Locatiekanalen sheet -->
|
||||
<string name="location_channels_title">#locatiekanalen</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">دوستانہ گلے ملنا پھجو</string>
|
||||
<string name="action_block_title">%1$s نوں بلاک کرو</string>
|
||||
<string name="action_block_subtitle">اس یوزر دے سارے پیغام بلاک کرو</string>
|
||||
<string name="action_delete_note_title">ਨੋਟ ਮਿਟਾਓ</string>
|
||||
<string name="action_delete_note_subtitle">ਰਿਲੇਅਾਂ ਨੂੰ nostr ਮਿਟਾਉਣ ਦੀ ਬੇਨਤੀ ਭੇਜਦਾ ਹੈ</string>
|
||||
|
||||
<!-- geohash چینل شیٹ -->
|
||||
<string name="location_channels_title">#لوکیشن چینل</string>
|
||||
|
||||
@ -53,5 +53,7 @@
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="action_delete_note_title">usuń notatkę</string>
|
||||
<string name="action_delete_note_subtitle">wysyła żądanie usunięcia nostr do przekaźników</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">Enviar um abraço amigável</string>
|
||||
<string name="action_block_title">Bloquear %1$s</string>
|
||||
<string name="action_block_subtitle">Bloquear todas as mensagens deste usuário</string>
|
||||
<string name="action_delete_note_title">excluir nota</string>
|
||||
<string name="action_delete_note_subtitle">envia uma solicitação de exclusão nostr para os relays</string>
|
||||
|
||||
<!-- Canais geohash -->
|
||||
<string name="location_channels_title">#Canais de localização</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">Enviar um abraço amigável</string>
|
||||
<string name="action_block_title">Bloquear %1$s</string>
|
||||
<string name="action_block_subtitle">Bloquear todas as mensagens deste utilizador</string>
|
||||
<string name="action_delete_note_title">eliminar nota</string>
|
||||
<string name="action_delete_note_subtitle">envia um pedido de eliminação nostr para os relays</string>
|
||||
|
||||
<!-- Folha de canais geohash -->
|
||||
<string name="location_channels_title">#Canais de localização</string>
|
||||
|
||||
@ -172,6 +172,8 @@
|
||||
<string name="action_hug_subtitle">отправить дружеские объятия</string>
|
||||
<string name="action_block_title">заблокировать %1$s</string>
|
||||
<string name="action_block_subtitle">заблокировать все сообщения от этого пользователя</string>
|
||||
<string name="action_delete_note_title">удалить заметку</string>
|
||||
<string name="action_delete_note_subtitle">отправляет запрос на удаление nostr в ретрансляторы</string>
|
||||
|
||||
<string name="location_channels_title">#каналы локации</string>
|
||||
<string name="location_channels_desc">общайся с людьми рядом через каналы geohash. делится только грубый geohash, никогда точный gps. не делай скриншоты и не делись экраном для защиты приватности.</string>
|
||||
|
||||
@ -172,6 +172,8 @@
|
||||
<string name="action_hug_subtitle">skicka en vänlig kram</string>
|
||||
<string name="action_block_title">blockera %1$s</string>
|
||||
<string name="action_block_subtitle">blockera alla meddelanden från den här användaren</string>
|
||||
<string name="action_delete_note_title">ta bort anteckning</string>
|
||||
<string name="action_delete_note_subtitle">skickar en nostr-borttagningsbegäran till reläer</string>
|
||||
|
||||
<string name="location_channels_title">#platskanaler</string>
|
||||
<string name="location_channels_desc">chatta med folk nära dig via geohash‑kanaler. endast en grov geohash delas, aldrig exakt gps. ta inte skärmdumpar eller dela skärmen för att skydda din integritet.</string>
|
||||
|
||||
@ -40,5 +40,7 @@
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="action_delete_note_title">குறிப்பை நீக்கு</string>
|
||||
<string name="action_delete_note_subtitle">relay-களுக்கு nostr நீக்கும் கோரிக்கையை அனுப்புகிறது</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">ส่งกอดแบบเป็นมิตร</string>
|
||||
<string name="action_block_title">บล็อก %1$s</string>
|
||||
<string name="action_block_subtitle">บล็อกข้อความทั้งหมดจากผู้ใช้นี้</string>
|
||||
<string name="action_delete_note_title">ลบบันทึก</string>
|
||||
<string name="action_delete_note_subtitle">ส่งคำขอลบ nostr ไปยัง relay</string>
|
||||
|
||||
<!-- แผ่นช่อง geohash -->
|
||||
<string name="location_channels_title">#ช่องตามตำแหน่ง</string>
|
||||
|
||||
@ -172,6 +172,8 @@
|
||||
<string name="action_hug_subtitle">samimi bir sarılma gönder</string>
|
||||
<string name="action_block_title">%1$s kişisini engelle</string>
|
||||
<string name="action_block_subtitle">bu kullanıcıdan gelen tüm mesajları engelle</string>
|
||||
<string name="action_delete_note_title">notu sil</string>
|
||||
<string name="action_delete_note_subtitle">rölelere bir nostr silme isteği gönderir</string>
|
||||
|
||||
<string name="location_channels_title">#konum kanalları</string>
|
||||
<string name="location_channels_desc">yakındaki insanlarla geohash kanallarıyla sohbet et. yalnızca kabaca geohash paylaşılır, kesin gps asla değil. gizliliğin için ekran görüntüsü alma ve paylaşma.</string>
|
||||
|
||||
@ -40,5 +40,7 @@
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="action_delete_note_title">видалити нотатку</string>
|
||||
<string name="action_delete_note_subtitle">надсилає запит на видалення nostr до ретрансляторів</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">دوستانہ آلنگن بھیجیں</string>
|
||||
<string name="action_block_title">%1$s کو بلاک کریں</string>
|
||||
<string name="action_block_subtitle">اس صارف کے تمام پیغامات بلاک کریں</string>
|
||||
<string name="action_delete_note_title">نوٹ حذف کریں</string>
|
||||
<string name="action_delete_note_subtitle">ریلے پر nostr حذف کی درخواست بھیجتا ہے</string>
|
||||
|
||||
<!-- geohash چینلز شیٹ -->
|
||||
<string name="location_channels_title">#مقام چینلز</string>
|
||||
|
||||
@ -192,6 +192,8 @@
|
||||
<string name="action_hug_subtitle">Gửi một cái ôm thân thiện</string>
|
||||
<string name="action_block_title">Chặn %1$s</string>
|
||||
<string name="action_block_subtitle">Chặn tất cả tin nhắn từ người dùng này</string>
|
||||
<string name="action_delete_note_title">xóa ghi chú</string>
|
||||
<string name="action_delete_note_subtitle">gửi yêu cầu xóa nostr tới các relay</string>
|
||||
|
||||
<!-- Sheet kênh geohash -->
|
||||
<string name="location_channels_title">#Kênh vị trí</string>
|
||||
|
||||
@ -53,5 +53,7 @@
|
||||
<string name="verify_success_title">已验证</string>
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
<string name="action_delete_note_title">删除笔记</string>
|
||||
<string name="action_delete_note_subtitle">向中继发送 nostr 删除请求</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -53,5 +53,7 @@
|
||||
<string name="verify_success_title">已验证</string>
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
<string name="action_delete_note_title">刪除筆記</string>
|
||||
<string name="action_delete_note_subtitle">向中繼發送 nostr 刪除請求</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -185,6 +185,8 @@
|
||||
<string name="action_hug_subtitle">发送一个友好的拥抱</string>
|
||||
<string name="action_block_title">屏蔽 %1$s</string>
|
||||
<string name="action_block_subtitle">屏蔽该用户的所有消息</string>
|
||||
<string name="action_delete_note_title">删除笔记</string>
|
||||
<string name="action_delete_note_subtitle">向中继发送 nostr 删除请求</string>
|
||||
|
||||
<!-- 地点频道面板 -->
|
||||
<string name="location_channels_title">#地点频道</string>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<string name="no_one_connected">No one connected</string>
|
||||
<string name="emergency_clear_hint">Triple tap to clear all data</string>
|
||||
<string name="your_network">Network</string>
|
||||
|
||||
|
||||
<!-- Battery Optimization Strings -->
|
||||
<string name="battery_optimization_detected">Battery Optimization Detected</string>
|
||||
<string name="battery_optimization_disabled">Battery Optimization Disabled</string>
|
||||
@ -37,7 +37,7 @@
|
||||
<string name="battery_optimization_continue">Continue</string>
|
||||
<string name="retry">Retry</string>
|
||||
<string name="skip">Skip</string>
|
||||
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_summary_more">and %1$d more</string>
|
||||
<string name="notification_messages_from_people">%1$d messages from %2$d people</string>
|
||||
@ -201,10 +201,12 @@
|
||||
<string name="action_hug_title">hug %1$s</string>
|
||||
<string name="action_hug_subtitle">send a friendly hug message</string>
|
||||
<string name="action_block_title">block %1$s</string>
|
||||
|
||||
<string name="action_block_subtitle">block all messages from this user</string>
|
||||
<string name="action_private_message_title">message %1$s</string>
|
||||
<string name="action_private_message_subtitle">send a private message</string>
|
||||
|
||||
<string name="action_delete_note_title">delete note</string>
|
||||
<string name="action_delete_note_subtitle">sends a nostr deletion request to relays</string>
|
||||
|
||||
<!-- Location channels sheet -->
|
||||
<string name="location_channels_title">#location channels</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user