Group chat Ui improvements:

- Added Actions modal for one-click access to all actions
- Tweaked the top bar and bottom bars
- Made it bubbles
This commit is contained in:
Niel Liesmons
2026-04-09 22:10:48 +02:00
parent 32f973f814
commit f37032d1d0
7 changed files with 1377 additions and 587 deletions
+57
View File
@@ -0,0 +1,57 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
error message: The daemon has terminated unexpectedly on startup attempt #2 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
Problems may have occurred during auto-selection of GC. The preferred GC is Parallel GC.
If the problems persist, try adding the JVM option to the Kotlin daemon JVM arguments: -XX:-UseParallelGC.
GC auto-selection logic is disabled temporary for the next daemon startup.
error message: The daemon has terminated unexpectedly on startup attempt #3 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
error message: Failed connecting to the daemon in 4 retries
error message: Daemon compilation failed: Could not connect to Kotlin compile daemon
java.lang.RuntimeException: Could not connect to Kotlin compile daemon
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
+19
View File
@@ -0,0 +1,19 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Wisp: Run on Emulator",
"type": "node-terminal",
"request": "launch",
"command": "echo 'Running Wisp on emulator...'",
"preLaunchTask": "wisp:run-on-emulator"
},
{
"name": "Wisp: Start Filtered Logcat",
"type": "node-terminal",
"request": "launch",
"command": "echo 'Starting filtered logcat...'",
"preLaunchTask": "wisp:logcat"
}
]
}
+55
View File
@@ -0,0 +1,55 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "wisp:start-emulator",
"type": "shell",
"command": "if adb devices | awk '/^emulator-/{found=1} END{exit !found}'; then echo \"Emulator already running\"; else nohup \"$HOME/Library/Android/sdk/emulator/emulator\" -avd Pixel_9_API_35 >/tmp/wisp-emulator.log 2>&1 & echo \"Starting emulator Pixel_9_API_35\"; fi",
"problemMatcher": []
},
{
"label": "wisp:wait-for-boot",
"type": "shell",
"command": "adb wait-for-device; echo \"Waiting for Android boot (Package Manager)...\"; i=0; while [ \"$i\" -lt 180 ]; do b=$(adb shell getprop sys.boot_completed 2>/dev/null | xargs); if [ \"$b\" = \"1\" ]; then echo \"Boot complete.\"; exit 0; fi; i=$((i+1)); sleep 2; done; echo \"Timeout: emulator did not finish booting.\"; exit 1",
"problemMatcher": []
},
{
"label": "wisp:prepare-device-space",
"type": "shell",
"command": "adb uninstall com.wisp.app >/dev/null 2>&1 || true; adb shell pm trim-caches 2G >/dev/null 2>&1 || true",
"problemMatcher": []
},
{
"label": "wisp:install-debug",
"type": "shell",
"command": "export JAVA_HOME=\"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home\"; export PATH=\"$JAVA_HOME/bin:$PATH\"; ./gradlew :app:assembleDebug && SERIAL=$(adb devices | awk '/^emulator-.*[[:space:]]+device$/{print $1; exit}'); [ -n \"$SERIAL\" ] || SERIAL=$(adb devices | awk '/^[[:alnum:]:.-]+[[:space:]]+device$/{print $1; exit}'); [ -n \"$SERIAL\" ] || { echo \"No adb device in state device\"; exit 1; }; adb -s \"$SERIAL\" install -r app/build/outputs/apk/debug/app-debug.apk",
"group": "build",
"problemMatcher": []
},
{
"label": "wisp:launch-app",
"type": "shell",
"command": "adb wait-for-device && adb shell monkey -p com.wisp.app -c android.intent.category.LAUNCHER 1",
"problemMatcher": []
},
{
"label": "wisp:run-on-emulator",
"dependsOrder": "sequence",
"dependsOn": [
"wisp:start-emulator",
"wisp:wait-for-boot",
"wisp:prepare-device-space",
"wisp:install-debug",
"wisp:launch-app"
],
"problemMatcher": []
},
{
"label": "wisp:logcat",
"type": "shell",
"command": "if command -v rg >/dev/null 2>&1; then adb logcat | rg --line-buffered \"AndroidRuntime|FATAL EXCEPTION|System.err|wisp|Wisp\"; else adb logcat; fi",
"isBackground": true,
"problemMatcher": []
}
]
}
+29 -4
View File
@@ -1553,6 +1553,7 @@ fun WispNavHost(
val groupRoomUploadScope = rememberCoroutineScope()
var groupRoomUploadProgress by remember { mutableStateOf<String?>(null) }
var groupRoomZapTarget by remember { mutableStateOf<com.wisp.app.nostr.NostrEvent?>(null) }
var groupRoomZapInitialSats by remember { mutableStateOf<Int?>(null) }
var showGroupRoomEmojiLibrary by remember { mutableStateOf(false) }
val groupRoomResolvedEmojis by feedViewModel.customEmojiRepo.resolvedEmojis.collectAsState()
val groupRoomUnicodeEmojis by feedViewModel.customEmojiRepo.sortedUnicodeEmojis.collectAsState()
@@ -1580,14 +1581,19 @@ fun WispNavHost(
}
ZapDialog(
isWalletConnected = isNwcConnected,
onDismiss = { groupRoomZapTarget = null },
onDismiss = {
groupRoomZapTarget = null
groupRoomZapInitialSats = null
},
onZap = { amountMsats, message, isAnonymous, isPrivate ->
val event = groupRoomZapTarget ?: return@ZapDialog
groupRoomZapTarget = null
groupRoomZapInitialSats = null
feedViewModel.sendZap(event, amountMsats, message, isAnonymous, isPrivate)
},
onGoToWallet = { navController.navigateSafe(Routes.WALLET) },
canPrivateZap = feedViewModel.relayPool.hasDmRelays() && recipientHasDmRelays
canPrivateZap = feedViewModel.relayPool.hasDmRelays() && recipientHasDmRelays,
initialSatsHint = groupRoomZapInitialSats
)
}
val groupRoomMediaLauncher = rememberLauncherForActivityResult(
@@ -1652,6 +1658,27 @@ fun WispNavHost(
uploadProgress = groupRoomUploadProgress,
myPubkey = feedViewModel.getUserPubkey(),
onZap = { msgId, senderPubkey ->
groupRoomZapInitialSats = null
groupRoomZapTarget = com.wisp.app.nostr.NostrEvent(
id = msgId,
pubkey = senderPubkey,
created_at = 0L,
kind = com.wisp.app.nostr.Nip29.KIND_CHAT_MESSAGE,
tags = listOf(listOf("h", groupId)),
content = "",
sig = ""
)
feedViewModel.relayPool.sendToRelayOrEphemeral(
relayUrl,
com.wisp.app.nostr.ClientMessage.req(
subscriptionId = "zap-rcpt-grp-${msgId.take(12)}",
filter = com.wisp.app.nostr.Filter(kinds = listOf(9735), eTags = listOf(msgId))
),
skipBadCheck = true
)
},
onZapPreset = { msgId, senderPubkey, sats ->
groupRoomZapInitialSats = sats
groupRoomZapTarget = com.wisp.app.nostr.NostrEvent(
id = msgId,
pubkey = senderPubkey,
@@ -1661,8 +1688,6 @@ fun WispNavHost(
content = "",
sig = ""
)
// Subscribe to zap receipt on the group relay too, in case the
// LNURL provider publishes it there
feedViewModel.relayPool.sendToRelayOrEphemeral(
relayUrl,
com.wisp.app.nostr.ClientMessage.req(
@@ -53,6 +53,7 @@ import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -102,7 +103,9 @@ fun ZapDialog(
onDismiss: () -> Unit,
onZap: (amountMsats: Long, message: String, isAnonymous: Boolean, isPrivate: Boolean) -> Unit,
onGoToWallet: () -> Unit,
canPrivateZap: Boolean = false
canPrivateZap: Boolean = false,
/** When opening from a quick preset (e.g. chat actions sheet), pre-select that amount in sats. */
initialSatsHint: Int? = null
) {
if (!isWalletConnected) {
AlertDialog(
@@ -134,6 +137,22 @@ fun ZapDialog(
var isPrivate by remember { mutableStateOf(false) }
var editMode by remember { mutableStateOf(false) }
LaunchedEffect(initialSatsHint) {
val hint = initialSatsHint ?: return@LaunchedEffect
val h = hint.toLong().coerceAtLeast(1L)
val fromPrefs = ZapPreferences(context).getPresets().sortedBy { it.amountSats }
val match = fromPrefs.find { it.amountSats == h }
if (match != null) {
selectedPreset = match
isCustom = false
message = match.message
} else {
isCustom = true
customAmount = h.toString()
message = ""
}
}
val effectiveAmount = if (isCustom) {
customAmount.toLongOrNull() ?: 0L
} else {
File diff suppressed because it is too large Load Diff
+10
View File
@@ -238,6 +238,16 @@
<string name="error_no_relay_scores">No relay scores available yet</string>
<string name="btn_add_to_set">+Set</string>
<string name="btn_details">Details</string>
<string name="group_room_comment">Comment</string>
<string name="group_room_section_zap">Zap</string>
<string name="group_room_section_actions">Actions</string>
<string name="group_room_eyebrow_react">REACT</string>
<string name="group_room_eyebrow_zap">ZAP</string>
<string name="group_room_eyebrow_actions">ACTIONS</string>
<string name="cd_open_full_zap">Open zap dialog</string>
<string name="cd_open_reaction_picker">Open reaction picker</string>
<string name="cd_send_options">Send options</string>
<string name="send_options_coming_soon">Send options coming soon.</string>
<!-- Content Descriptions -->
<string name="cd_back">Back</string>