refactor(sonar): merge chained if-else into when

Sonar — convert 'if (A) {…} else if (B) {…} else {…}' chains into
'when { A -> …; B -> …; else -> … }' across 33 files (~44 sites).
The change is mechanically equivalent: same conditions, same branch
order, same short-circuit evaluation, no public-signature impact.

The 70-branch event-type dispatch in ThreadFeedView.kt:594 is intentionally
left as if-else for now — the conversion would balloon to a 280-line diff
with no behavioural change, making review impractical.
This commit is contained in:
davotoula
2026-05-13 10:40:38 +02:00
parent 5e179326d4
commit 8ed7e63427
33 changed files with 1399 additions and 1104 deletions
@@ -2859,14 +2859,11 @@ class Account(
if (event == null) return null
return if (isWriteable()) {
if (event is PrivateDmEvent) {
privateDMDecryptionCache.cachedDM(event)
} else if (event is LnZapRequestEvent && event.isPrivateZap()) {
privateZapsDecryptionCache.cachedPrivateZap(event)?.content
} else if (event is DraftWrapEvent) {
draftsDecryptionCache.preCachedDraft(event)?.content
} else {
event.content
when {
event is PrivateDmEvent -> privateDMDecryptionCache.cachedDM(event)
event is LnZapRequestEvent && event.isPrivateZap() -> privateZapsDecryptionCache.cachedPrivateZap(event)?.content
event is DraftWrapEvent -> draftsDecryptionCache.preCachedDraft(event)?.content
else -> event.content
}
} else {
event.content
@@ -2875,22 +2872,30 @@ class Account(
suspend fun decryptContent(note: Note): String? {
val event = note.event
return if (event is PrivateDmEvent && isWriteable()) {
privateDMDecryptionCache.decryptDM(event)
} else if (event is LnZapRequestEvent && isWriteable()) {
if (event.isPrivateZap()) {
if (isWriteable()) {
privateZapsDecryptionCache.decryptPrivateZap(event)?.content
} else {
null
}
} else {
event.content
return when {
event is PrivateDmEvent && isWriteable() -> {
privateDMDecryptionCache.decryptDM(event)
}
event is LnZapRequestEvent && isWriteable() -> {
if (event.isPrivateZap()) {
if (isWriteable()) {
privateZapsDecryptionCache.decryptPrivateZap(event)?.content
} else {
null
}
} else {
event.content
}
}
event is DraftWrapEvent && isWriteable() -> {
draftsDecryptionCache.cachedDraft(event)?.content
}
else -> {
event?.content
}
} else if (event is DraftWrapEvent && isWriteable()) {
draftsDecryptionCache.cachedDraft(event)?.content
} else {
event?.content
}
}
@@ -88,48 +88,56 @@ class ZapPaymentHandler(
val zapSplitSetup = noteEvent?.zapSplitSetup()
val unverifiedZapsToSend =
if (!zapSplitSetup.isNullOrEmpty()) {
zapSplitSetup.map { setup ->
when (setup) {
is ZapSplitSetupLnAddress -> {
UnverifiedZapSplitSetup(
lnAddress = setup.lnAddress,
weight = setup.weight,
)
}
when {
!zapSplitSetup.isNullOrEmpty() -> {
zapSplitSetup.map { setup ->
when (setup) {
is ZapSplitSetupLnAddress -> {
UnverifiedZapSplitSetup(
lnAddress = setup.lnAddress,
weight = setup.weight,
)
}
is ZapSplitSetup -> {
val user = LocalCache.checkGetOrCreateUser(setup.pubKeyHex)
UnverifiedZapSplitSetup(
lnAddress = user?.lnAddress(),
weight = setup.weight,
relay = setup.relay,
user = user,
)
is ZapSplitSetup -> {
val user = LocalCache.checkGetOrCreateUser(setup.pubKeyHex)
UnverifiedZapSplitSetup(
lnAddress = user?.lnAddress(),
weight = setup.weight,
relay = setup.relay,
user = user,
)
}
}
}
}
} else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) {
noteEvent.hosts().map {
val user = LocalCache.checkGetOrCreateUser(it.pubKey)
val lnAddress = user?.lnAddress()
UnverifiedZapSplitSetup(lnAddress, relay = it.relayHint, user = user)
noteEvent is LiveActivitiesEvent && noteEvent.hasHost() -> {
noteEvent.hosts().map {
val user = LocalCache.checkGetOrCreateUser(it.pubKey)
val lnAddress = user?.lnAddress()
UnverifiedZapSplitSetup(lnAddress, relay = it.relayHint, user = user)
}
}
} else if (noteEvent is AppDefinitionEvent) {
val appLud16 = noteEvent.appMetaData()?.lnAddress()
if (appLud16 != null) {
listOf(UnverifiedZapSplitSetup(appLud16))
} else {
val lud16 =
note.author?.lnAddress()
listOf(UnverifiedZapSplitSetup(lud16))
noteEvent is AppDefinitionEvent -> {
val appLud16 = noteEvent.appMetaData()?.lnAddress()
if (appLud16 != null) {
listOf(UnverifiedZapSplitSetup(appLud16))
} else {
val lud16 =
note.author?.lnAddress()
listOf(UnverifiedZapSplitSetup(lud16))
}
}
else -> {
listOf(
UnverifiedZapSplitSetup(
note.author?.lnAddress(),
),
)
}
} else {
listOf(
UnverifiedZapSplitSetup(
note.author?.lnAddress(),
),
)
}
if (showErrorIfNoLnAddress) {
@@ -63,18 +63,28 @@ class UrlPreview {
val mimeType =
response.headers["Content-Type"]?.toMediaType()
?: throw IllegalArgumentException("Website returned unknown mimetype: ${response.headers["Content-Type"]}")
if (mimeType.type == "text" && mimeType.subtype == "html") {
val metaTags = HtmlParser().parseHtml(response.body.source(), mimeType.charset())
val data = OpenGraphParser().extractUrlInfo(metaTags)
UrlInfoItem(url, data.title, data.description, data.image, mimeType.toString())
} else if (mimeType.type == "image") {
UrlInfoItem(url, image = url, mimeType = mimeType.toString())
} else if (mimeType.type == "video") {
UrlInfoItem(url, image = url, mimeType = mimeType.toString())
} else if (mimeType.type == "application" && mimeType.subtype == "pdf") {
UrlInfoItem(url, image = url, mimeType = mimeType.toString())
} else {
throw IllegalArgumentException("Website returned unknown encoding for previews: $mimeType")
when {
mimeType.type == "text" && mimeType.subtype == "html" -> {
val metaTags = HtmlParser().parseHtml(response.body.source(), mimeType.charset())
val data = OpenGraphParser().extractUrlInfo(metaTags)
UrlInfoItem(url, data.title, data.description, data.image, mimeType.toString())
}
mimeType.type == "image" -> {
UrlInfoItem(url, image = url, mimeType = mimeType.toString())
}
mimeType.type == "video" -> {
UrlInfoItem(url, image = url, mimeType = mimeType.toString())
}
mimeType.type == "application" && mimeType.subtype == "pdf" -> {
UrlInfoItem(url, image = url, mimeType = mimeType.toString())
}
else -> {
throw IllegalArgumentException("Website returned unknown encoding for previews: $mimeType")
}
}
} else {
throw IllegalArgumentException("Website returned: " + response.code)
@@ -70,31 +70,39 @@ class RelayProxyClientConnector(
RelayServiceInfra(torSettings, torConnection, clearConnection, connectivity, torStatus)
}.debounce(100)
.onEach {
if (it.connectivity is ConnectivityStatus.StartingService) {
// ignore
} else if (it.connectivity is ConnectivityStatus.Off) {
Log.d("ManageRelayServices") { "Connectivity Off: Pausing Relay Services ${it.connectivity}" }
if (client.isActive()) {
client.disconnect()
}
if (it.torStatus is TorServiceStatus.Active) {
Log.d("ManageRelayServices", "Connectivity off, Tor idle")
}
} else if (it.connectivity is ConnectivityStatus.Active && !client.isActive()) {
Log.d("ManageRelayServices", "Connectivity On: Resuming Relay Services")
if (it.torStatus is TorServiceStatus.Active) {
Log.d("ManageRelayServices", "Connectivity resumed, Tor active")
when {
it.connectivity is ConnectivityStatus.StartingService -> {
// ignore
}
// only calls this if the client is not active. Otherwise goes to the else below
client.connect()
} else {
Log.d("ManageRelayServices", "Relay Services have changed, reconnecting relays that need to")
client.reconnect(
onlyIfChanged = true,
ignoreRetryDelays = true,
)
it.connectivity is ConnectivityStatus.Off -> {
Log.d("ManageRelayServices") { "Connectivity Off: Pausing Relay Services ${it.connectivity}" }
if (client.isActive()) {
client.disconnect()
}
if (it.torStatus is TorServiceStatus.Active) {
Log.d("ManageRelayServices", "Connectivity off, Tor idle")
}
}
it.connectivity is ConnectivityStatus.Active && !client.isActive() -> {
Log.d("ManageRelayServices", "Connectivity On: Resuming Relay Services")
if (it.torStatus is TorServiceStatus.Active) {
Log.d("ManageRelayServices", "Connectivity resumed, Tor active")
}
// only calls this if the client is not active. Otherwise goes to the else below
client.connect()
}
else -> {
Log.d("ManageRelayServices", "Relay Services have changed, reconnecting relays that need to")
client.reconnect(
onlyIfChanged = true,
ignoreRetryDelays = true,
)
}
}
}.onStart {
Log.d("ManageRelayServices", "Resuming Relay Services")
@@ -136,14 +136,11 @@ class BlossomServerResolver(
url: String,
mimeType: String?,
): OkHttpClient =
if (mimeType == null) {
httpClientBuilder.okHttpClientForPreview(url)
} else if (mimeType.startsWith("audio/") || mimeType.startsWith("video/")) {
httpClientBuilder.okHttpClientForVideo(url)
} else if (mimeType.startsWith("image/")) {
httpClientBuilder.okHttpClientForImage(url)
} else {
httpClientBuilder.okHttpClientForPreview(url)
when {
mimeType == null -> httpClientBuilder.okHttpClientForPreview(url)
mimeType.startsWith("audio/") || mimeType.startsWith("video/") -> httpClientBuilder.okHttpClientForVideo(url)
mimeType.startsWith("image/") -> httpClientBuilder.okHttpClientForImage(url)
else -> httpClientBuilder.okHttpClientForPreview(url)
}
fun canResolve(scheme: String) = scheme == SCHEME
@@ -212,32 +212,38 @@ fun EditPostView(
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp)) {
if (RichTextParser.isValidURL(myUrlPreview)) {
if (RichTextParser.isImageUrl(myUrlPreview)) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier =
Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder,
),
)
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
} else {
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
when {
RichTextParser.isImageUrl(myUrlPreview) -> {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier =
Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = QuoteBorder)
.border(
1.dp,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder,
),
)
}
RichTextParser.isVideoUrl(myUrlPreview) -> {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
else -> {
LoadUrlPreview(myUrlPreview, myUrlPreview, null, accountViewModel)
}
}
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colorScheme.background
@@ -188,55 +188,67 @@ class NewMessageTagger(
key = key.removePrefix("@")
try {
if (key.startsWith("nsec1", true)) {
if (key.length < 63) {
return null
when {
key.startsWith("nsec1", true) -> {
if (key.length < 63) {
return null
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
// Converts to npub
val pubkey =
Nip19Parser.uriToRoute(Nip01Crypto.pubKeyCreate(keyB32.bechToBytes()).toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
// Converts to npub
val pubkey =
Nip19Parser.uriToRoute(Nip01Crypto.pubKeyCreate(keyB32.bechToBytes()).toNpub()) ?: return null
key.startsWith("npub1", true) -> {
if (key.length < 63) {
return null
}
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
} else if (key.startsWith("npub1", true)) {
if (key.length < 63) {
return null
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val pubkey = Nip19Parser.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
key.startsWith("note1", true) -> {
if (key.length < 63) {
return null
}
val pubkey = Nip19Parser.uriToRoute(keyB32) ?: return null
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
return DirtyKeyInfo(pubkey, restOfWord.ifEmpty { null })
} else if (key.startsWith("note1", true)) {
if (key.length < 63) {
return null
val noteId = Nip19Parser.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord.ifEmpty { null })
}
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
key.startsWith("nprofile", true) -> {
val pubkeyRelay = Nip19Parser.uriToRoute(key) ?: return null
val noteId = Nip19Parser.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
}
return DirtyKeyInfo(noteId, restOfWord.ifEmpty { null })
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19Parser.uriToRoute(key) ?: return null
key.startsWith("nevent1", true) -> {
val noteRelayId = Nip19Parser.uriToRoute(key) ?: return null
return DirtyKeyInfo(pubkeyRelay, pubkeyRelay.additionalChars)
} else if (key.startsWith("nevent1", true)) {
val noteRelayId = Nip19Parser.uriToRoute(key) ?: return null
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
}
return DirtyKeyInfo(noteRelayId, noteRelayId.additionalChars)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19Parser.uriToRoute(key) ?: return null
key.startsWith("naddr1", true) -> {
val address = Nip19Parser.uriToRoute(key) ?: return null
return DirtyKeyInfo(
address,
address.additionalChars,
) // no way to know when they address ends and dirt begins
return DirtyKeyInfo(
address,
address.additionalChars,
) // no way to know when they address ends and dirt begins
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -113,51 +113,69 @@ fun ShowImageGallery(
media: SelectedMedia,
accountViewModel: AccountViewModel,
) {
if (media.isImage() == true) {
AsyncImage(
model = media.uri.toString(),
contentDescription = media.uri.toString(),
contentScale = ContentScale.Crop,
modifier =
Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
)
} else if (media.isAudio() == true) {
FilePreviewPlaceholder(
icon = MaterialSymbols.AudioFile,
label = media.mimeType ?: "audio/*",
)
} else if (media.isDocument()) {
FilePreviewPlaceholder(
icon = MaterialSymbols.PictureAsPdf,
label = media.mimeType ?: "application/pdf",
)
} else if (media.isVideo() == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
val context = LocalContext.current
when {
media.isImage() == true -> {
AsyncImage(
model = media.uri.toString(),
contentDescription = media.uri.toString(),
contentScale = ContentScale.Crop,
modifier =
Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
)
}
LaunchedEffect(key1 = media) {
launch(Dispatchers.IO) {
try {
bitmap = createVideoThumb(context, media.uri)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
media.isAudio() == true -> {
FilePreviewPlaceholder(
icon = MaterialSymbols.AudioFile,
label = media.mimeType ?: "audio/*",
)
}
media.isDocument() -> {
FilePreviewPlaceholder(
icon = MaterialSymbols.PictureAsPdf,
label = media.mimeType ?: "application/pdf",
)
}
media.isVideo() == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
val context = LocalContext.current
LaunchedEffect(key1 = media) {
launch(Dispatchers.IO) {
try {
bitmap = createVideoThumb(context, media.uri)
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e)
}
}
}
if (bitmap != null) {
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "some useful description",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth(),
)
}
} else {
VideoView(
videoUri = media.uri.toString(),
mimeType = media.mimeType,
roundedCorner = false,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
if (bitmap != null) {
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "some useful description",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth(),
)
}
} else {
else -> {
VideoView(
videoUri = media.uri.toString(),
mimeType = media.mimeType,
@@ -166,14 +184,6 @@ fun ShowImageGallery(
accountViewModel = accountViewModel,
)
}
} else {
VideoView(
videoUri = media.uri.toString(),
mimeType = media.mimeType,
roundedCorner = false,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
@@ -95,34 +95,42 @@ fun RenderLoaded(
callbackUri: String? = null,
accountViewModel: AccountViewModel,
) {
if (state.previewInfo.mimeType.startsWith("image")) {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlImage(url, uri = callbackUri),
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
when {
state.previewInfo.mimeType.startsWith("image") -> {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlImage(url, uri = callbackUri),
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
}
} else if (state.previewInfo.mimeType.startsWith("video")) {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlVideo(url, uri = callbackUri),
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
state.previewInfo.mimeType.startsWith("video") -> {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlVideo(url, uri = callbackUri),
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
}
} else if (state.previewInfo.mimeType.startsWith("application/pdf")) {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlPdf(url, uri = callbackUri, mimeType = state.previewInfo.mimeType),
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
state.previewInfo.mimeType.startsWith("application/pdf") -> {
Box(modifier = HalfVertPadding) {
ZoomableContentView(
content = MediaUrlPdf(url, uri = callbackUri, mimeType = state.previewInfo.mimeType),
roundedCorner = true,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
}
else -> {
UrlPreviewCard(url, state.previewInfo)
}
} else {
UrlPreviewCard(url, state.previewInfo)
}
}
@@ -293,40 +293,48 @@ private fun handleZapClick(
val choices = zapAmountChoices ?: accountViewModel.zapAmountChoices()
if (choices.isEmpty()) {
accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change),
)
} else if (!accountViewModel.isWriteable()) {
accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps),
)
} else if (choices.size == 1) {
val amount = choices.first()
if (amount > 1100 || zapAmountChoices != null) {
onZapStarts()
accountViewModel.zap(
baseNote,
amount * 1000,
null,
"",
context,
showErrorIfNoLnAddress = false,
onError = onError,
onProgress = { onZappingProgress(it) },
onPayViaIntent = onPayViaIntent,
when {
choices.isEmpty() -> {
accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.no_zap_amount_setup_long_press_to_change),
)
} else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
}
} else {
if (choices.any { it > 1100 } || zapAmountChoices != null) {
onMultipleChoices(choices)
} else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
!accountViewModel.isWriteable() -> {
accountViewModel.toastManager.toast(
stringRes(context, R.string.error_dialog_zap_error),
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps),
)
}
choices.size == 1 -> {
val amount = choices.first()
if (amount > 1100 || zapAmountChoices != null) {
onZapStarts()
accountViewModel.zap(
baseNote,
amount * 1000,
null,
"",
context,
showErrorIfNoLnAddress = false,
onError = onError,
onProgress = { onZappingProgress(it) },
onPayViaIntent = onPayViaIntent,
)
} else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
}
}
else -> {
if (choices.any { it > 1100 } || zapAmountChoices != null) {
onMultipleChoices(choices)
} else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
}
}
}
}
@@ -67,25 +67,31 @@ fun WatchBlockAndReport(
mutableStateOf(false)
}
if (showAnyway.value) {
normalNote(true)
} else if (!isHidden.isPostHidden) {
if (isHidden.isAcceptable) {
normalNote(isHidden.canPreview)
} else {
HiddenNote(
isHidden.relevantReports,
isHidden.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showAnyway.value = true },
)
when {
showAnyway.value -> {
normalNote(true)
}
} else if (showHiddenWarning) {
// if it is a quoted or boosted note, how the hidden warning.
HiddenNoteByMe {
showAnyway.value = true
!isHidden.isPostHidden -> {
if (isHidden.isAcceptable) {
normalNote(isHidden.canPreview)
} else {
HiddenNote(
isHidden.relevantReports,
isHidden.isHiddenAuthor,
accountViewModel,
modifier,
nav,
onClick = { showAnyway.value = true },
)
}
}
showHiddenWarning -> {
// if it is a quoted or boosted note, how the hidden warning.
HiddenNoteByMe {
showAnyway.value = true
}
}
}
}
@@ -293,78 +293,86 @@ fun DisplayStatusInner(
)
}
if (url != null) {
val uri = LocalUriHandler.current
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
onClick = { runCatching { uri.openUri(url.trim()) } },
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
when {
url != null -> {
val uri = LocalUriHandler.current
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
}
} else if (nostrATag != null) {
LoadAddressableNote(nostrATag, accountViewModel) { note ->
if (note != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
onClick = { runCatching { uri.openUri(url.trim()) } },
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
modifier = Size15Modifier,
onClick = {
routeFor(
note,
accountViewModel.account,
)?.let { nav.nav(it) }
},
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
}
}
nostrATag != null -> {
LoadAddressableNote(nostrATag, accountViewModel) { note ->
if (note != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
onClick = {
routeFor(
note,
accountViewModel.account,
)?.let { nav.nav(it) }
},
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
}
}
}
}
} else if (nostrETag != null) {
LoadNote(baseNoteHex = nostrETag.eventId, accountViewModel) {
if (it != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
onClick = {
routeFor(
it,
accountViewModel.account,
)?.let { nav.nav(it) }
},
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
nostrETag != null -> {
LoadNote(baseNoteHex = nostrETag.eventId, accountViewModel) {
if (it != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
onClick = {
routeFor(
it,
accountViewModel.account,
)?.let { nav.nav(it) }
},
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
}
}
}
}
} else if (nostrPTag != null) {
LoadUser(baseUserHex = nostrPTag, accountViewModel) { user ->
if (user != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
onClick = { nav.nav(routeForUser(nostrPTag)) },
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
nostrPTag != null -> {
LoadUser(baseUserHex = nostrPTag, accountViewModel) { user ->
if (user != null) {
Spacer(modifier = StdHorzSpacer)
IconButton(
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
onClick = { nav.nav(routeForUser(nostrPTag)) },
) {
Icon(
symbol = MaterialSymbols.AutoMirrored.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.lessImportantLink,
)
}
}
}
}
@@ -116,14 +116,11 @@ val graspLink = { graspNumber: String ->
val externalLinkForNote = { note: Note ->
if (note is AddressableNote) {
if (note.event?.bountyBaseReward() != null) {
"https://nostrbounties.com/b/${note.toNAddr()}"
} else if (note.event is PeopleListEvent) {
"https://listr.lol/a/${note.toNAddr()}"
} else if (note.event is FollowListEvent) {
"https://following.space/d/${note.address.dTag}?p=${note.address.pubKeyHex}"
} else {
njumpLink(note.toNAddr())
when {
note.event?.bountyBaseReward() != null -> "https://nostrbounties.com/b/${note.toNAddr()}"
note.event is PeopleListEvent -> "https://listr.lol/a/${note.toNAddr()}"
note.event is FollowListEvent -> "https://following.space/d/${note.address.dTag}?p=${note.address.pubKeyHex}"
else -> njumpLink(note.toNAddr())
}
} else {
njumpLink(note.toNEvent())
@@ -1146,20 +1146,28 @@ private fun likeClick(
}
val choices = accountViewModel.reactionChoices()
if (choices.isEmpty()) {
accountViewModel.toastManager.toast(
R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change,
)
} else if (!accountViewModel.isWriteable()) {
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts,
)
} else if (choices.size == 1) {
onWantsToSignReaction()
} else if (choices.size > 1) {
onMultipleChoices()
when {
choices.isEmpty() -> {
accountViewModel.toastManager.toast(
R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change,
)
}
!accountViewModel.isWriteable() -> {
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts,
)
}
choices.size == 1 -> {
onWantsToSignReaction()
}
choices.size > 1 -> {
onMultipleChoices()
}
}
}
@@ -1369,27 +1377,35 @@ fun zapClick(
val choices = accountViewModel.zapAmountChoices()
if (choices.isEmpty()) {
onCustomAmount()
} else if (!accountViewModel.isWriteable()) {
accountViewModel.toastManager.toast(
R.string.error_dialog_zap_error,
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
)
} else if (choices.size == 1) {
onZapStarts()
accountViewModel.zap(
baseNote,
choices.first() * 1000,
null,
"",
context,
onError = onError,
onProgress = { onZappingProgress(it) },
onPayViaIntent = onPayViaIntent,
)
} else if (choices.size > 1) {
onMultipleChoices()
when {
choices.isEmpty() -> {
onCustomAmount()
}
!accountViewModel.isWriteable() -> {
accountViewModel.toastManager.toast(
R.string.error_dialog_zap_error,
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
)
}
choices.size == 1 -> {
onZapStarts()
accountViewModel.zap(
baseNote,
choices.first() * 1000,
null,
"",
context,
onError = onError,
onProgress = { onZappingProgress(it) },
onPayViaIntent = onPayViaIntent,
)
}
choices.size > 1 -> {
onMultipleChoices()
}
}
}
@@ -56,34 +56,45 @@ fun timeAgo(
val timeDifference = TimeUtils.now() - time
return if (timeDifference > TimeUtils.ONE_YEAR) {
// Dec 12, 2022
return when {
timeDifference > TimeUtils.ONE_YEAR -> {
// Dec 12, 2022
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
}
prefix + yearFormatter.format(time * 1000)
}
prefix + yearFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_MONTH) {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
timeDifference > TimeUtils.ONE_MONTH -> {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
}
prefix + monthFormatter.format(time * 1000)
}
prefix + monthFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_DAY) {
// 2 days
prefix + (timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, days)
} else if (timeDifference > TimeUtils.ONE_HOUR) {
prefix + (timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, hours)
} else if (timeDifference > TimeUtils.ONE_MINUTE) {
prefix + (timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, minutes)
} else {
prefix + stringRes(context, seconds)
timeDifference > TimeUtils.ONE_DAY -> {
prefix + (timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, days)
}
timeDifference > TimeUtils.ONE_HOUR -> {
prefix + (timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, hours)
}
timeDifference > TimeUtils.ONE_MINUTE -> {
prefix + (timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, minutes)
}
else -> {
prefix + stringRes(context, seconds)
}
}
}
@@ -96,34 +107,45 @@ fun timeAgoNoDot(
val timeDifference = TimeUtils.now() - time
return if (timeDifference > TimeUtils.ONE_YEAR) {
// Dec 12, 2022
return when {
timeDifference > TimeUtils.ONE_YEAR -> {
// Dec 12, 2022
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
}
yearFormatter.format(time * 1000)
}
yearFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_MONTH) {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
timeDifference > TimeUtils.ONE_MONTH -> {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
}
monthFormatter.format(time * 1000)
}
monthFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_DAY) {
// 2 days
(timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, R.string.d)
} else if (timeDifference > TimeUtils.ONE_HOUR) {
(timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, R.string.h)
} else if (timeDifference > TimeUtils.ONE_MINUTE) {
(timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, R.string.m)
} else {
stringRes(context, R.string.now)
timeDifference > TimeUtils.ONE_DAY -> {
(timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, R.string.d)
}
timeDifference > TimeUtils.ONE_HOUR -> {
(timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, R.string.h)
}
timeDifference > TimeUtils.ONE_MINUTE -> {
(timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, R.string.m)
}
else -> {
stringRes(context, R.string.now)
}
}
}
@@ -136,34 +158,45 @@ fun timeAgoNoDotNoDay(
val timeDifference = TimeUtils.now() - time
return if (timeDifference > TimeUtils.ONE_YEAR) {
// Dec 12, 2022
return when {
timeDifference > TimeUtils.ONE_YEAR -> {
// Dec 12, 2022
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
}
yearNoDayFormatter.format(time * 1000)
}
yearNoDayFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_MONTH) {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
timeDifference > TimeUtils.ONE_MONTH -> {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
}
monthNoDayFormatter.format(time * 1000)
}
monthNoDayFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_DAY) {
// 2 days
(timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, R.string.d)
} else if (timeDifference > TimeUtils.ONE_HOUR) {
(timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, R.string.h)
} else if (timeDifference > TimeUtils.ONE_MINUTE) {
(timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, R.string.m)
} else {
stringRes(context, R.string.now)
timeDifference > TimeUtils.ONE_DAY -> {
(timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, R.string.d)
}
timeDifference > TimeUtils.ONE_HOUR -> {
(timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, R.string.h)
}
timeDifference > TimeUtils.ONE_MINUTE -> {
(timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, R.string.m)
}
else -> {
stringRes(context, R.string.now)
}
}
}
@@ -176,34 +209,45 @@ fun timeAheadNoDot(
val timeDifference = time - TimeUtils.now()
return if (timeDifference > TimeUtils.ONE_YEAR) {
// Dec 12, 2022
return when {
timeDifference > TimeUtils.ONE_YEAR -> {
// Dec 12, 2022
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
}
yearFormatter.format(time * 1000)
}
yearFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_MONTH) {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
timeDifference > TimeUtils.ONE_MONTH -> {
// Dec 12
if (locale != Locale.getDefault()) {
locale = Locale.getDefault()
yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
}
monthFormatter.format(time * 1000)
}
monthFormatter.format(time * 1000)
} else if (timeDifference > TimeUtils.ONE_DAY) {
// 2 days
round(timeDifference / TimeUtils.ONE_DAY.toFloat()).toInt().toString() + stringRes(context, R.string.d)
} else if (timeDifference > TimeUtils.ONE_HOUR) {
round(timeDifference / TimeUtils.ONE_HOUR.toFloat()).toInt().toString() + stringRes(context, R.string.h)
} else if (timeDifference > TimeUtils.ONE_MINUTE) {
round(timeDifference / TimeUtils.ONE_MINUTE.toFloat()).toInt().toString() + stringRes(context, R.string.m)
} else {
stringRes(context, R.string.now)
timeDifference > TimeUtils.ONE_DAY -> {
round(timeDifference / TimeUtils.ONE_DAY.toFloat()).toInt().toString() + stringRes(context, R.string.d)
}
timeDifference > TimeUtils.ONE_HOUR -> {
round(timeDifference / TimeUtils.ONE_HOUR.toFloat()).toInt().toString() + stringRes(context, R.string.h)
}
timeDifference > TimeUtils.ONE_MINUTE -> {
round(timeDifference / TimeUtils.ONE_MINUTE.toFloat()).toInt().toString() + stringRes(context, R.string.m)
}
else -> {
stringRes(context, R.string.now)
}
}
}
@@ -547,59 +547,69 @@ fun ZapVote(
interactionSource = remember { MutableInteractionSource() },
indication = ripple24dp,
onClick = {
if (!accountViewModel.isWriteable()) {
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
)
} else if (pollViewModel.isPollClosed()) {
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.poll_is_closed_explainer,
)
} else if (isLoggedUser) {
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.poll_author_no_vote,
)
} else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn.value) {
// only allow one vote per option when min==max, i.e. atomic vote amount specified
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.one_vote_per_user_on_atomic_votes,
)
return@combinedClickable
} else if (
when {
!accountViewModel.isWriteable() -> {
accountViewModel.toastManager.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_send_zaps,
)
}
pollViewModel.isPollClosed() -> {
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.poll_is_closed_explainer,
)
}
isLoggedUser -> {
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.poll_author_no_vote,
)
}
pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn.value -> {
// only allow one vote per option when min==max, i.e. atomic vote amount specified
accountViewModel.toastManager.toast(
R.string.poll_unable_to_vote,
R.string.one_vote_per_user_on_atomic_votes,
)
return@combinedClickable
}
accountViewModel.zapAmountChoices().size == 1 &&
pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first())
) {
accountViewModel.zap(
baseNote,
accountViewModel.zapAmountChoices().first() * 1000,
poolOption.option,
"",
context,
onError = { title, message, user ->
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(title, message)
},
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
onPayViaIntent = {
if (it.size == 1) {
val payable = it.first()
payViaIntent(payable.invoice, context, { }) { error ->
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(stringRes(context, R.string.error_dialog_zap_error), error)
pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first()) -> {
accountViewModel.zap(
baseNote,
accountViewModel.zapAmountChoices().first() * 1000,
poolOption.option,
"",
context,
onError = { title, message, user ->
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(title, message)
},
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
onPayViaIntent = {
if (it.size == 1) {
val payable = it.first()
payViaIntent(payable.invoice, context, { }) { error ->
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(stringRes(context, R.string.error_dialog_zap_error), error)
}
} else {
val uid = Uuid.random().toString()
accountViewModel.tempManualPaymentCache.put(uid, it)
nav.nav(Route.ManualZapSplitPayment(uid))
}
} else {
val uid = Uuid.random().toString()
accountViewModel.tempManualPaymentCache.put(uid, it)
nav.nav(Route.ManualZapSplitPayment(uid))
}
},
)
} else {
wantsToZap = true
},
)
}
else -> {
wantsToZap = true
}
}
},
),
@@ -138,14 +138,11 @@ class PollNoteViewModel : ViewModel() {
} == true
fun voteAmountPlaceHolderText(sats: String): String =
if (valueMinimum == null && valueMaximum == null) {
sats
} else if (valueMinimum == null) {
"1—$valueMaximum $sats"
} else if (valueMaximum == null) {
">$valueMinimum $sats"
} else {
"$valueMinimum$valueMaximum $sats"
when {
valueMinimum == null && valueMaximum == null -> sats
valueMinimum == null -> "1—$valueMaximum $sats"
valueMaximum == null -> ">$valueMinimum $sats"
else -> "$valueMinimum$valueMaximum $sats"
}
fun inputVoteAmountLong(textAmount: String) =
@@ -160,46 +157,66 @@ class PollNoteViewModel : ViewModel() {
}
fun isValidInputVoteAmount(amount: BigDecimal?): Boolean {
if (amount == null) {
return false
} else if (valueMinimum == null && valueMaximum == null) {
if (amount > BigDecimal.ZERO) {
return true
when {
amount == null -> {
return false
}
} else if (valueMinimum == null) {
if (amount > BigDecimal.ZERO && amount <= valueMaximumBD!!) {
return true
valueMinimum == null && valueMaximum == null -> {
if (amount > BigDecimal.ZERO) {
return true
}
}
} else if (valueMaximum == null) {
if (amount >= valueMinimumBD!!) {
return true
valueMinimum == null -> {
if (amount > BigDecimal.ZERO && amount <= valueMaximumBD!!) {
return true
}
}
} else {
if ((valueMinimumBD!! <= amount) && (amount <= valueMaximumBD!!)) {
return true
valueMaximum == null -> {
if (amount >= valueMinimumBD!!) {
return true
}
}
else -> {
if ((valueMinimumBD!! <= amount) && (amount <= valueMaximumBD!!)) {
return true
}
}
}
return false
}
fun isValidInputVoteAmount(amount: Long?): Boolean {
if (amount == null) {
return false
} else if (valueMinimum == null && valueMaximum == null) {
if (amount > 0) {
return true
when {
amount == null -> {
return false
}
} else if (valueMinimum == null) {
if (amount > 0 && amount <= valueMaximum!!) {
return true
valueMinimum == null && valueMaximum == null -> {
if (amount > 0) {
return true
}
}
} else if (valueMaximum == null) {
if (amount >= valueMinimum!!) {
return true
valueMinimum == null -> {
if (amount > 0 && amount <= valueMaximum!!) {
return true
}
}
} else {
if ((valueMinimum!! <= amount) && (amount <= valueMaximum!!)) {
return true
valueMaximum == null -> {
if (amount >= valueMinimum!!) {
return true
}
}
else -> {
if ((valueMinimum!! <= amount) && (amount <= valueMaximum!!)) {
return true
}
}
}
return false
@@ -60,40 +60,52 @@ fun PreviewUrl(
accountViewModel: AccountViewModel,
nav: INav,
) {
if (RichTextParser.isValidURL(myUrlPreview)) {
if (RichTextParser.isImageUrl(myUrlPreview)) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxHeight().aspectRatio(1f),
)
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = false,
gallery = false,
contentScale = ContentScale.FillHeight,
accountViewModel = accountViewModel,
)
} else {
MyLoadUrlPreviewDirect(myUrlPreview, myUrlPreview, accountViewModel)
}
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf(bgColor) }
when {
RichTextParser.isValidURL(myUrlPreview) -> {
when {
RichTextParser.isImageUrl(myUrlPreview) -> {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillHeight,
modifier = Modifier.fillMaxHeight().aspectRatio(1f),
)
}
BechLinkPreview(
word = myUrlPreview,
canPreview = true,
quotesLeft = 1,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
MyLoadUrlPreviewDirect("https://$myUrlPreview", myUrlPreview, accountViewModel)
RichTextParser.isVideoUrl(myUrlPreview) -> {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = false,
gallery = false,
contentScale = ContentScale.FillHeight,
accountViewModel = accountViewModel,
)
}
else -> {
MyLoadUrlPreviewDirect(myUrlPreview, myUrlPreview, accountViewModel)
}
}
}
RichTextParser.startsWithNIP19Scheme(myUrlPreview) -> {
val bgColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf(bgColor) }
BechLinkPreview(
word = myUrlPreview,
canPreview = true,
quotesLeft = 1,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
RichTextParser.isUrlWithoutScheme(myUrlPreview) -> {
MyLoadUrlPreviewDirect("https://$myUrlPreview", myUrlPreview, accountViewModel)
}
}
}
@@ -104,24 +116,30 @@ fun PreviewUrlFillWidth(
nav: INav,
) {
if (RichTextParser.isValidURL(myUrlPreview)) {
if (RichTextParser.isImageUrl(myUrlPreview)) {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxHeight().aspectRatio(1f),
)
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = false,
gallery = false,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
} else {
MyLoadUrlPreviewDirectFillWidth(myUrlPreview, myUrlPreview, accountViewModel)
when {
RichTextParser.isImageUrl(myUrlPreview) -> {
AsyncImage(
model = myUrlPreview,
contentDescription = myUrlPreview,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxHeight().aspectRatio(1f),
)
}
RichTextParser.isVideoUrl(myUrlPreview) -> {
VideoView(
myUrlPreview,
mimeType = null,
roundedCorner = false,
gallery = false,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
else -> {
MyLoadUrlPreviewDirectFillWidth(myUrlPreview, myUrlPreview, accountViewModel)
}
}
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
val bgColor = MaterialTheme.colorScheme.background
@@ -92,50 +92,58 @@ class UserSuggestionState(
} else {
null
}
if (nip05 != null) {
runCatching {
nip05Client.get(nip05)?.let { info ->
val user = account.cache.checkGetOrCreateUser(info.pubkey)
if (user != null) {
info.relays.forEach {
it.normalizeRelayUrlOrNull()?.let { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
when {
nip05 != null -> {
runCatching {
nip05Client.get(nip05)?.let { info ->
val user = account.cache.checkGetOrCreateUser(info.pubkey)
if (user != null) {
info.relays.forEach {
it.normalizeRelayUrlOrNull()?.let { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
}
}
}
user
}
}.getOrNull()
}
prefix.startsWithAny(userUriPrefixes) -> {
runCatching {
Nip19Parser.uriToRoute(prefix)?.entity?.let { parsed ->
when (parsed) {
is NSec -> {
account.cache.getOrCreateUser(parsed.toPubKey().toHexKey())
}
is NPub -> {
account.cache.getOrCreateUser(parsed.hex)
}
is NProfile -> {
val user = account.cache.getOrCreateUser(parsed.hex)
parsed.relay.forEach { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
}
user
}
else -> {
null
}
}
}
user
}
}.getOrNull()
} else if (prefix.startsWithAny(userUriPrefixes)) {
runCatching {
Nip19Parser.uriToRoute(prefix)?.entity?.let { parsed ->
when (parsed) {
is NSec -> {
account.cache.getOrCreateUser(parsed.toPubKey().toHexKey())
}
}.getOrNull()
}
is NPub -> {
account.cache.getOrCreateUser(parsed.hex)
}
prefix.length == 64 && Hex.isHex64(prefix) -> {
account.cache.getOrCreateUser(prefix)
}
is NProfile -> {
val user = account.cache.getOrCreateUser(parsed.hex)
parsed.relay.forEach { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
}
user
}
else -> {
null
}
}
}
}.getOrNull()
} else if (prefix.length == 64 && Hex.isHex64(prefix)) {
account.cache.getOrCreateUser(prefix)
} else {
null
else -> {
null
}
}
} else {
null
@@ -331,60 +331,66 @@ fun RenderAttestationRequest(
}
if (quotesLeft > 0) {
if (aboutAddress != null) {
LoadAddressableNote(aboutAddress, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
isQuotedNote = true,
unPackReply = ReplyRenderType.NONE,
makeItShort = true,
quotesLeft = quotesLeft - 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
when {
aboutAddress != null -> {
LoadAddressableNote(aboutAddress, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
isQuotedNote = true,
unPackReply = ReplyRenderType.NONE,
makeItShort = true,
quotesLeft = quotesLeft - 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
} else if (aboutEvent != null) {
LoadNote(aboutEvent, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
isQuotedNote = true,
unPackReply = ReplyRenderType.NONE,
makeItShort = true,
quotesLeft = quotesLeft - 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
aboutEvent != null -> {
LoadNote(aboutEvent, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
NoteCompose(
baseNote = it,
modifier = MaterialTheme.colorScheme.replyModifier,
isQuotedNote = true,
unPackReply = ReplyRenderType.NONE,
makeItShort = true,
quotesLeft = quotesLeft - 1,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
} else if (aboutPubkey != null) {
LoadUser(aboutPubkey, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
aboutPubkey != null -> {
LoadUser(aboutPubkey, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
Text(
text = stringRes(R.string.attestation_requests_attestation_to),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
}
}
}
}
@@ -278,21 +278,29 @@ private fun DisplayQuoteAuthor(
}
}
if (addressable != null) {
addressable?.let {
DisplayEntryForNote(it, userBase, accountViewModel, nav)
when {
addressable != null -> {
addressable?.let {
DisplayEntryForNote(it, userBase, accountViewModel, nav)
}
}
} else if (version != null) {
version?.let {
DisplayEntryForNote(it, userBase, accountViewModel, nav)
}
} else if (baseUrl != null) {
val url = "$baseUrl${if (baseUrl.contains("#")) "&" else "#"}:~:text=${Uri.encode(highlightQuote)}"
DisplayEntryForAUrl(url, userBase, accountViewModel, nav)
} else if (userBase != null) {
userBase?.let {
DisplayEntryForUser(it, accountViewModel, nav)
version != null -> {
version?.let {
DisplayEntryForNote(it, userBase, accountViewModel, nav)
}
}
baseUrl != null -> {
val url = "$baseUrl${if (baseUrl.contains("#")) "&" else "#"}:~:text=${Uri.encode(highlightQuote)}"
DisplayEntryForAUrl(url, userBase, accountViewModel, nav)
}
userBase != null -> {
userBase?.let {
DisplayEntryForUser(it, accountViewModel, nav)
}
}
}
}
@@ -233,14 +233,11 @@ fun RenderEyeGlassesPrescription(
val isContacts = contactsRightEye != null || contactsLeftEye != null
Text(
if (isGlasses && isContacts) {
"Vision Prescription"
} else if (isGlasses) {
"Glasses Prescription"
} else if (isContacts) {
"Contact Lenses Prescription"
} else {
"Empty Prescription"
when {
isGlasses && isContacts -> "Vision Prescription"
isGlasses -> "Glasses Prescription"
isContacts -> "Contact Lenses Prescription"
else -> "Empty Prescription"
},
modifier = Modifier.padding(4.dp).fillMaxWidth(),
textAlign = TextAlign.Center,
@@ -134,35 +134,45 @@ class AccountSessionManager(
}
val accountSettings =
if (loginWithExternalSigner) {
AccountSettings(
keyPair = KeyPair(pubKey = pubKeyParsed),
transientAccount = transientAccount,
externalSignerPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" },
)
} else if (key.startsWith("nsec")) {
val privHex =
decodePrivateKeyAsHexOrNull(key)
?: throw Exception("Invalid nsec key")
AccountSettings(
keyPair = KeyPair(privKey = privHex.hexToByteArray()),
transientAccount = transientAccount,
)
} else if (key.contains(" ") && Nip06().isValidMnemonic(key)) {
AccountSettings(
keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)),
transientAccount = transientAccount,
)
} else if (pubKeyParsed != null) {
AccountSettings(
keyPair = KeyPair(pubKey = pubKeyParsed),
transientAccount = transientAccount,
)
} else {
AccountSettings(
keyPair = KeyPair(Hex.decode(key)),
transientAccount = transientAccount,
)
when {
loginWithExternalSigner -> {
AccountSettings(
keyPair = KeyPair(pubKey = pubKeyParsed),
transientAccount = transientAccount,
externalSignerPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" },
)
}
key.startsWith("nsec") -> {
val privHex =
decodePrivateKeyAsHexOrNull(key)
?: throw Exception("Invalid nsec key")
AccountSettings(
keyPair = KeyPair(privKey = privHex.hexToByteArray()),
transientAccount = transientAccount,
)
}
key.contains(" ") && Nip06().isValidMnemonic(key) -> {
AccountSettings(
keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)),
transientAccount = transientAccount,
)
}
pubKeyParsed != null -> {
AccountSettings(
keyPair = KeyPair(pubKey = pubKeyParsed),
transientAccount = transientAccount,
)
}
else -> {
AccountSettings(
keyPair = KeyPair(Hex.decode(key)),
transientAccount = transientAccount,
)
}
}
localPreferences.setDefaultAccount(accountSettings)
@@ -1404,16 +1404,22 @@ class AccountViewModel(
viewModelScope.launch(Dispatchers.IO) {
for (note in notes) {
val noteEvent = note.event
if (noteEvent is IsInPublicChatChannel) {
account.markAsRead("Channel/${noteEvent.channelId()}", noteEvent.createdAt)
} else if (noteEvent is ChatroomKeyable) {
account.markAsRead("Room/${noteEvent.chatroomKey(account.signer.pubKey).hashCode()}", noteEvent.createdAt)
} else if (noteEvent is DraftWrapEvent) {
val innerEvent = account.draftsDecryptionCache.preCachedDraft(noteEvent)
if (innerEvent is IsInPublicChatChannel) {
account.markAsRead("Channel/${innerEvent.channelId()}", noteEvent.createdAt)
} else if (innerEvent is ChatroomKeyable) {
account.markAsRead("Room/${innerEvent.chatroomKey(account.signer.pubKey).hashCode()}", noteEvent.createdAt)
when {
noteEvent is IsInPublicChatChannel -> {
account.markAsRead("Channel/${noteEvent.channelId()}", noteEvent.createdAt)
}
noteEvent is ChatroomKeyable -> {
account.markAsRead("Room/${noteEvent.chatroomKey(account.signer.pubKey).hashCode()}", noteEvent.createdAt)
}
noteEvent is DraftWrapEvent -> {
val innerEvent = account.draftsDecryptionCache.preCachedDraft(noteEvent)
if (innerEvent is IsInPublicChatChannel) {
account.markAsRead("Channel/${innerEvent.channelId()}", noteEvent.createdAt)
} else if (innerEvent is ChatroomKeyable) {
account.markAsRead("Room/${innerEvent.chatroomKey(account.signer.pubKey).hashCode()}", noteEvent.createdAt)
}
}
}
}
@@ -172,15 +172,17 @@ fun NormalChatNote(
remember(note) {
derivedStateOf {
val noteEvent = note.event
if (accountViewModel.isLoggedUser(note.author)) {
false // never shows the user's pictures
} else if (noteEvent is PrivateDmEvent) {
false // one-on-one, never shows it.
} else if (noteEvent is ChatroomKeyable) {
when {
accountViewModel.isLoggedUser(note.author) -> false
// never shows the user's pictures
noteEvent is PrivateDmEvent -> false
// one-on-one, never shows it.
// only shows in a group chat.
noteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex).users.size > 1
} else {
true
noteEvent is ChatroomKeyable -> noteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex).users.size > 1
else -> true
}
}
}
@@ -735,17 +735,25 @@ class ChatNewMessageViewModel :
fun autocompleteWithUser(item: User) {
userSuggestions?.let { userSuggestions ->
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
val lastWord = message.currentWord()
userSuggestions.replaceCurrentWord(message, lastWord, item)
urlPreviews.update(message.text.toString())
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
val lastWord = toUsers.currentWord()
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
updateRoomFromUsersInput()
when (userSuggestionsMainMessage) {
UserSuggestionAnchor.MAIN_MESSAGE -> {
val lastWord = message.currentWord()
userSuggestions.replaceCurrentWord(message, lastWord, item)
urlPreviews.update(message.text.toString())
}
UserSuggestionAnchor.FORWARD_ZAPS -> {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
}
UserSuggestionAnchor.TO_USERS -> {
val lastWord = toUsers.currentWord()
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
updateRoomFromUsersInput()
}
else -> {}
}
userSuggestionsMainMessage = null
@@ -415,85 +415,106 @@ open class ChannelNewMessageViewModel :
val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null
val localExpirationDate = if (wantsExpirationDate) expirationDate else null
return if (channel is PublicChatChannel) {
val replyingToEvent = replyTo.value?.toEventHint<ChannelMessageEvent>()
val channelEvent = channel.event
return when {
channel is PublicChatChannel -> {
val replyingToEvent = replyTo.value?.toEventHint<ChannelMessageEvent>()
val channelEvent = channel.event
if (replyingToEvent != null) {
ChannelMessageEvent.reply(tagger.message, replyingToEvent) {
notify(replyingToEvent.toPTag())
if (replyingToEvent != null) {
ChannelMessageEvent.reply(tagger.message, replyingToEvent) {
notify(replyingToEvent.toPTag())
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
geoHash?.let { geohash(it) }
geoHash?.let { geohash(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else if (channelEvent != null) {
val hint = EventHintBundle(channelEvent, channelRelays.firstOrNull())
ChannelMessageEvent.message(tagger.message, hint) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else if (channelEvent != null) {
val hint = EventHintBundle(channelEvent, channelRelays.firstOrNull())
ChannelMessageEvent.message(tagger.message, hint) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
geoHash?.let { geohash(it) }
geoHash?.let { geohash(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else {
ChannelMessageEvent.message(tagger.message, ETag(channel.idHex, channelRelays.firstOrNull())) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else {
ChannelMessageEvent.message(tagger.message, ETag(channel.idHex, channelRelays.firstOrNull())) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
geoHash?.let { geohash(it) }
geoHash?.let { geohash(it) }
emojis(emojis)
imetas(usedAttachments)
emojis(emojis)
imetas(usedAttachments)
}
}
}
} else if (channel is LiveActivitiesChannel) {
val replyingToEvent = replyTo.value?.toEventHint<LiveActivitiesChatMessageEvent>()
val activity = channel.info
if (replyingToEvent != null) {
LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) {
notify(replyingToEvent.toPTag())
channel is LiveActivitiesChannel -> {
val replyingToEvent = replyTo.value?.toEventHint<LiveActivitiesChatMessageEvent>()
val activity = channel.info
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
if (replyingToEvent != null) {
LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) {
notify(replyingToEvent.toPTag())
emojis(emojis)
imetas(usedAttachments)
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else if (activity != null) {
val hint = EventHintBundle(activity, channelRelays.firstOrNull())
LiveActivitiesChatMessageEvent.message(tagger.message, hint) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else {
LiveActivitiesChatMessageEvent.message(tagger.message, channel.toATag()) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
}
} else if (activity != null) {
val hint = EventHintBundle(activity, channelRelays.firstOrNull())
}
LiveActivitiesChatMessageEvent.message(tagger.message, hint) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
} else {
LiveActivitiesChatMessageEvent.message(tagger.message, channel.toATag()) {
channel is EphemeralChatChannel -> {
EphemeralChatEvent.build(
tagger.message,
channel.roomId.relayUrl,
channel.roomId.id,
) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
@@ -504,23 +525,10 @@ open class ChannelNewMessageViewModel :
imetas(usedAttachments)
}
}
} else if (channel is EphemeralChatChannel) {
EphemeralChatEvent.build(
tagger.message,
channel.roomId.relayUrl,
channel.roomId.id,
) {
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
else -> {
null
}
} else {
null
}
}
@@ -140,18 +140,26 @@ fun RenderContentDVMThumb(
card.amount?.let {
var color = Color.DarkGray
var amount = it
if (card.amount == "free" || card.amount == "0") {
color = MaterialTheme.colorScheme.secondary
amount = "Free"
} else if (card.amount == "flexible") {
color = MaterialTheme.colorScheme.primaryContainer
amount = "Flexible"
} else if (card.amount == "") {
color = MaterialTheme.colorScheme.grayText
amount = "Unknown"
} else {
color = MaterialTheme.colorScheme.primary
amount = card.amount + " Sats"
when {
card.amount == "free" || card.amount == "0" -> {
color = MaterialTheme.colorScheme.secondary
amount = "Free"
}
card.amount == "flexible" -> {
color = MaterialTheme.colorScheme.primaryContainer
amount = "Flexible"
}
card.amount == "" -> {
color = MaterialTheme.colorScheme.grayText
amount = "Unknown"
}
else -> {
color = MaterialTheme.colorScheme.primary
amount = card.amount + " Sats"
}
}
Text(
textAlign = TextAlign.End,
@@ -98,41 +98,48 @@ class NotificationSummaryState(
LocalCache.notes.forEach { _, it ->
val noteEvent = it.event
if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id)) {
if (noteEvent is ReactionEvent) {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
reactions[netDate] = (reactions[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
}
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
boosts[netDate] = (boosts[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
}
} else if (noteEvent is LnZapEvent) {
// the user might be sending his own receipts noteEvent.pubKey != currentUser
if (noteEvent.isTaggedUser(currentUser)) {
val netDate = formatDate(noteEvent.createdAt)
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
takenIntoAccount.add(noteEvent.id)
}
} else if (noteEvent is BaseThreadedEvent &&
noteEvent.isTaggedUser(currentUser) &&
noteEvent.pubKey != currentUser
) {
val isCitation =
noteEvent.findCitations().any {
LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == currentUser
when {
noteEvent is ReactionEvent -> {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
reactions[netDate] = (reactions[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
}
val netDate = formatDate(noteEvent.createdAt)
if (isCitation) {
boosts[netDate] = (boosts[netDate] ?: 0) + 1
} else {
replies[netDate] = (replies[netDate] ?: 0) + 1
}
takenIntoAccount.add(noteEvent.id)
noteEvent is RepostEvent || noteEvent is GenericRepostEvent -> {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
boosts[netDate] = (boosts[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
}
}
noteEvent is LnZapEvent -> {
// the user might be sending his own receipts noteEvent.pubKey != currentUser
if (noteEvent.isTaggedUser(currentUser)) {
val netDate = formatDate(noteEvent.createdAt)
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
takenIntoAccount.add(noteEvent.id)
}
}
noteEvent is BaseThreadedEvent &&
noteEvent.isTaggedUser(currentUser) &&
noteEvent.pubKey != currentUser -> {
val isCitation =
noteEvent.findCitations().any {
LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == currentUser
}
val netDate = formatDate(noteEvent.createdAt)
if (isCitation) {
boosts[netDate] = (boosts[netDate] ?: 0) + 1
} else {
replies[netDate] = (replies[netDate] ?: 0) + 1
}
takenIntoAccount.add(noteEvent.id)
}
}
}
}
@@ -162,45 +169,52 @@ class NotificationSummaryState(
newNotes.forEach {
val noteEvent = it.event
if (noteEvent != null && !takenIntoAccount.contains(noteEvent.id)) {
if (noteEvent is ReactionEvent) {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
reactions[netDate] = (reactions[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
}
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
boosts[netDate] = (boosts[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
}
} else if (noteEvent is LnZapEvent) {
if (noteEvent.isTaggedUser(currentUser)) {
// && noteEvent.pubKey != currentUser User might be sending his own receipts
val netDate = formatDate(noteEvent.createdAt)
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
}
} else if (noteEvent is BaseThreadedEvent &&
noteEvent.isTaggedUser(currentUser) &&
noteEvent.pubKey != currentUser
) {
val isCitation =
noteEvent.findCitations().any {
LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == currentUser
when {
noteEvent is ReactionEvent -> {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
reactions[netDate] = (reactions[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
}
val netDate = formatDate(noteEvent.createdAt)
if (isCitation) {
boosts[netDate] = (boosts[netDate] ?: 0) + 1
} else {
replies[netDate] = (replies[netDate] ?: 0) + 1
}
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
noteEvent is RepostEvent || noteEvent is GenericRepostEvent -> {
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
val netDate = formatDate(noteEvent.createdAt)
boosts[netDate] = (boosts[netDate] ?: 0) + 1
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
}
}
noteEvent is LnZapEvent -> {
if (noteEvent.isTaggedUser(currentUser)) {
// && noteEvent.pubKey != currentUser User might be sending his own receipts
val netDate = formatDate(noteEvent.createdAt)
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
}
}
noteEvent is BaseThreadedEvent &&
noteEvent.isTaggedUser(currentUser) &&
noteEvent.pubKey != currentUser -> {
val isCitation =
noteEvent.findCitations().any {
LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == currentUser
}
val netDate = formatDate(noteEvent.createdAt)
if (isCitation) {
boosts[netDate] = (boosts[netDate] ?: 0) + 1
} else {
replies[netDate] = (replies[netDate] ?: 0) + 1
}
takenIntoAccount.add(noteEvent.id)
hasNewElements = true
}
}
}
}
@@ -598,16 +598,24 @@ class NewPublicMessageViewModel :
fun autocompleteWithUser(item: User) {
userSuggestions?.let { userSuggestions ->
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
val lastWord = message.currentWord()
userSuggestions.replaceCurrentWord(message, lastWord, item)
urlPreviews.update(message.text.toString())
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
val lastWord = toUsers.currentWord()
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
when (userSuggestionsMainMessage) {
UserSuggestionAnchor.MAIN_MESSAGE -> {
val lastWord = message.currentWord()
userSuggestions.replaceCurrentWord(message, lastWord, item)
urlPreviews.update(message.text.toString())
}
UserSuggestionAnchor.FORWARD_ZAPS -> {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
}
UserSuggestionAnchor.TO_USERS -> {
val lastWord = toUsers.currentWord()
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
}
else -> {}
}
userSuggestionsMainMessage = null
@@ -79,100 +79,110 @@ fun GalleryThumbnail(
val noteEvent = noteState.note.event ?: return
val content =
if (noteEvent is ProfileGalleryEntryEvent) {
noteEvent.urls().map { url ->
if (isVideoUrl(url)) {
MediaUrlVideo(
url = url,
description = noteEvent.content,
hash = null,
blurhash = noteEvent.blurhash(),
dim = noteEvent.dimensions(),
uri = null,
mimeType = noteEvent.mimeType(),
thumbhash = noteEvent.thumbhash(),
artworkUri = noteEvent.image()?.imageUrl ?: noteEvent.thumb()?.imageUrl,
)
} else {
when {
noteEvent is ProfileGalleryEntryEvent -> {
noteEvent.urls().map { url ->
if (isVideoUrl(url)) {
MediaUrlVideo(
url = url,
description = noteEvent.content,
hash = null,
blurhash = noteEvent.blurhash(),
dim = noteEvent.dimensions(),
uri = null,
mimeType = noteEvent.mimeType(),
thumbhash = noteEvent.thumbhash(),
artworkUri = noteEvent.image()?.imageUrl ?: noteEvent.thumb()?.imageUrl,
)
} else {
MediaUrlImage(
url = url,
description = noteEvent.content,
// We don't want to show the hash banner here
hash = null,
blurhash = noteEvent.blurhash(),
dim = noteEvent.dimensions(),
uri = null,
mimeType = noteEvent.mimeType(),
thumbhash = noteEvent.thumbhash(),
)
}
}
}
noteEvent is PictureEvent -> {
noteEvent.imetaTags().map { imeta ->
MediaUrlImage(
url = url,
url = imeta.url,
description = noteEvent.content,
// We don't want to show the hash banner here
hash = null,
blurhash = noteEvent.blurhash(),
dim = noteEvent.dimensions(),
blurhash = imeta.blurhash,
dim = imeta.dimension,
uri = null,
mimeType = noteEvent.mimeType(),
thumbhash = noteEvent.thumbhash(),
mimeType = imeta.mimeType,
thumbhash = imeta.thumbhash,
)
}
}
} else if (noteEvent is PictureEvent) {
noteEvent.imetaTags().map { imeta ->
MediaUrlImage(
url = imeta.url,
description = noteEvent.content,
// We don't want to show the hash banner here
hash = null,
blurhash = imeta.blurhash,
dim = imeta.dimension,
uri = null,
mimeType = imeta.mimeType,
thumbhash = imeta.thumbhash,
)
}
} else if (noteEvent is VideoEvent) {
// An HLS publish writes one imeta per rendition (master + each variant) on the same
// NIP-71 event. Rendering them all expands a single video into a sub-grid of black
// tiles inside one gallery card — the .m3u8 playlist is a text manifest Coil can't
// decode. Collapse to a single tile when every imeta is an HLS playlist; prefer an
// imeta that carries a poster image, breaking ties by smallest dimensions so we
// pick the lowest-resolution variant. Non-HLS multi-imeta videos keep the existing
// per-imeta layout.
val imetas = noteEvent.imetaTags()
val isAllHls = imetas.isNotEmpty() && imetas.all { isHlsMimeType(it.mimeType) }
val toRender =
if (isAllHls) {
val withPoster = imetas.filter { it.image.isNotEmpty() }
val pick =
withPoster.ifEmpty { imetas }.minBy {
it.dimension?.let { d -> d.width * d.height } ?: Int.MAX_VALUE
}
listOf(pick)
} else {
imetas
}
toRender.map { imeta ->
MediaUrlVideo(
url = imeta.url,
description = noteEvent.content,
// We don't want to show the hash banner here
hash = null,
blurhash = imeta.blurhash,
dim = imeta.dimension,
uri = null,
mimeType = imeta.mimeType,
thumbhash = imeta.thumbhash,
artworkUri = imeta.image.firstOrNull(),
)
}
} else if (noteEvent is LiveActivitiesClipEvent) {
noteEvent.videoUrl()?.let { url ->
listOf(
noteEvent is VideoEvent -> {
// An HLS publish writes one imeta per rendition (master + each variant) on the same
// NIP-71 event. Rendering them all expands a single video into a sub-grid of black
// tiles inside one gallery card — the .m3u8 playlist is a text manifest Coil can't
// decode. Collapse to a single tile when every imeta is an HLS playlist; prefer an
// imeta that carries a poster image, breaking ties by smallest dimensions so we
// pick the lowest-resolution variant. Non-HLS multi-imeta videos keep the existing
// per-imeta layout.
val imetas = noteEvent.imetaTags()
val isAllHls = imetas.isNotEmpty() && imetas.all { isHlsMimeType(it.mimeType) }
val toRender =
if (isAllHls) {
val withPoster = imetas.filter { it.image.isNotEmpty() }
val pick =
withPoster.ifEmpty { imetas }.minBy {
it.dimension?.let { d -> d.width * d.height } ?: Int.MAX_VALUE
}
listOf(pick)
} else {
imetas
}
toRender.map { imeta ->
MediaUrlVideo(
url = url,
description = noteEvent.title() ?: noteEvent.content,
url = imeta.url,
description = noteEvent.content,
// We don't want to show the hash banner here
hash = null,
blurhash = null,
dim = null,
blurhash = imeta.blurhash,
dim = imeta.dimension,
uri = null,
mimeType = null,
thumbhash = null,
),
)
} ?: emptyList()
} else {
emptyList()
mimeType = imeta.mimeType,
thumbhash = imeta.thumbhash,
artworkUri = imeta.image.firstOrNull(),
)
}
}
noteEvent is LiveActivitiesClipEvent -> {
noteEvent.videoUrl()?.let { url ->
listOf(
MediaUrlVideo(
url = url,
description = noteEvent.title() ?: noteEvent.content,
hash = null,
blurhash = null,
dim = null,
uri = null,
mimeType = null,
thumbhash = null,
),
)
} ?: emptyList()
}
else -> {
emptyList()
}
}
InnerRenderGalleryThumb(content, baseNote, accountViewModel)
@@ -125,50 +125,58 @@ class SearchBarViewModel(
} else {
null
}
if (nip05 != null) {
runCatching {
nip05Client.get(nip05)?.let { info ->
val user = account.cache.checkGetOrCreateUser(info.pubkey)
if (user != null) {
info.relays.forEach {
it.normalizeRelayUrlOrNull()?.let { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
when {
nip05 != null -> {
runCatching {
nip05Client.get(nip05)?.let { info ->
val user = account.cache.checkGetOrCreateUser(info.pubkey)
if (user != null) {
info.relays.forEach {
it.normalizeRelayUrlOrNull()?.let { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
}
}
}
user
}
}.getOrNull()
}
term.startsWithAny(userUriPrefixes) -> {
runCatching {
Nip19Parser.uriToRoute(term)?.entity?.let { parsed ->
when (parsed) {
is NSec -> {
account.cache.getOrCreateUser(parsed.toPubKey().toHexKey())
}
is NPub -> {
account.cache.getOrCreateUser(parsed.hex)
}
is NProfile -> {
val user = account.cache.getOrCreateUser(parsed.hex)
parsed.relay.forEach { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
}
user
}
else -> {
null
}
}
}
user
}
}.getOrNull()
} else if (term.startsWithAny(userUriPrefixes)) {
runCatching {
Nip19Parser.uriToRoute(term)?.entity?.let { parsed ->
when (parsed) {
is NSec -> {
account.cache.getOrCreateUser(parsed.toPubKey().toHexKey())
}
}.getOrNull()
}
is NPub -> {
account.cache.getOrCreateUser(parsed.hex)
}
term.length == 64 && Hex.isHex64(term) -> {
account.cache.getOrCreateUser(term)
}
is NProfile -> {
val user = account.cache.getOrCreateUser(parsed.hex)
parsed.relay.forEach { relay ->
account.cache.relayHints.addKey(user.pubkey(), relay)
}
user
}
else -> {
null
}
}
}
}.getOrNull()
} else if (term.length == 64 && Hex.isHex64(term)) {
account.cache.getOrCreateUser(term)
} else {
null
else -> {
null
}
}
}.flowOn(Dispatchers.IO)
@@ -168,34 +168,40 @@ fun VideoFeedLoaded(
key = { _, item -> item.idHex },
contentType = { _, item -> item.event?.kind ?: -1 },
) { _, item ->
if (item.event is PictureEvent) {
PictureCardCompose(
baseNote = item,
accountViewModel = accountViewModel,
nav = nav,
)
when {
item.event is PictureEvent -> {
PictureCardCompose(
baseNote = item,
accountViewModel = accountViewModel,
nav = nav,
)
HorizontalDivider(
thickness = DividerThickness,
)
HorizontalDivider(
thickness = DividerThickness,
)
Spacer(modifier = Modifier.height(8.dp))
} else if (item.event is VideoEvent) {
VideoCardCompose(item, accountViewModel, nav)
Spacer(modifier = Modifier.height(8.dp))
}
HorizontalDivider(
thickness = DividerThickness,
)
item.event is VideoEvent -> {
VideoCardCompose(item, accountViewModel, nav)
Spacer(modifier = Modifier.height(8.dp))
} else if (item.event is FileHeaderEvent) {
FileHeaderCardCompose(item, accountViewModel, nav)
HorizontalDivider(
thickness = DividerThickness,
)
HorizontalDivider(
thickness = DividerThickness,
)
Spacer(modifier = Modifier.height(8.dp))
}
Spacer(modifier = Modifier.height(8.dp))
item.event is FileHeaderEvent -> {
FileHeaderCardCompose(item, accountViewModel, nav)
HorizontalDivider(
thickness = DividerThickness,
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}