added NIP-09 event deletion request for location note

This commit is contained in:
Aleks Pliekhov 2026-03-19 18:38:07 -04:00 committed by Oleksandr Pliekhov
parent b7f0b33d3a
commit 817f86b440
39 changed files with 533 additions and 175 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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(),
)
}
},
)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>