mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
refactor: nest-room launch port, KMP ICS export and Marmot group notices
- AppPlatform.openNestRoom starts the audio room; the live bubble and the nest lobby call it instead of NestActivity.launch. - IcsExport moves to commonMain: UTC stamps and basic dates are computed on SearchDate instead of java.time (IcsExportTest still passes). - Marmot create/edit screens toast through the shared notices; VoiceReplyViewModel names exceptions with KClass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
@@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
@@ -37,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.AndroidAroundMeLocationLabel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.AppBottomBar as AppBottomBarImpl
|
||||
import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel as AppRememberManifestIconModel
|
||||
@@ -52,6 +54,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.legalSettingsCateg
|
||||
object AndroidAppPlatform : AppPlatform {
|
||||
override val isCastingAvailable: Boolean get() = BuildConfig.IS_CASTING_AVAILABLE
|
||||
|
||||
// NestActivity.launch adds FLAG_ACTIVITY_NEW_TASK, so the application context can start it.
|
||||
override fun openNestRoom(addressValue: String) = NestActivity.launch(Amethyst.instance.appContext, addressValue)
|
||||
|
||||
// Per flavour: Play links the hosted policies, F-Droid surfaces none.
|
||||
override fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = flavorLegalSettingsCategory(uriHandler)
|
||||
|
||||
|
||||
+3
-9
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
@@ -42,7 +41,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange
|
||||
import com.vitorpamplona.amethyst.commons.marmot.ui.MarmotRetentionChoice
|
||||
@@ -60,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_keypackage_relays_not
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_skip_for_now
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_use_outbox_relays
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
@@ -93,7 +92,7 @@ fun CreateGroupScreen(
|
||||
var isCreating by remember { mutableStateOf(false) }
|
||||
var showKeyPackageRelayDialog by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val longNotice = rememberLongNotice()
|
||||
|
||||
/**
|
||||
* Create the group, optionally after [prepare].
|
||||
@@ -154,12 +153,7 @@ fun CreateGroupScreen(
|
||||
} catch (e: Exception) {
|
||||
isCreating = false
|
||||
launch(Dispatchers.Main) {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
loadStringRes(Res.string.marmot_failed_to_create_group, e.message),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
longNotice.show(loadStringRes(Res.string.marmot_failed_to_create_group, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-12
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
@@ -40,7 +39,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange
|
||||
@@ -58,6 +56,8 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_group_name_placeholde
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_legacy_group_no_avatar_url
|
||||
import com.vitorpamplona.amethyst.commons.resources.save
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
@@ -92,7 +92,8 @@ fun EditGroupInfoScreen(
|
||||
var removeIcon by remember { mutableStateOf(false) }
|
||||
var isSaving by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val shortNotice = rememberShortNotice()
|
||||
val longNotice = rememberLongNotice()
|
||||
|
||||
val iconChanged = pickedIcon != null || removeIcon
|
||||
val avatarUrlChanged = avatarUrl.trim() != currentAvatarUrl?.url.orEmpty()
|
||||
@@ -136,20 +137,13 @@ fun EditGroupInfoScreen(
|
||||
accountViewModel.setMarmotGroupAvatarUrl(nostrGroupId, avatarUrl.trim())
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
Toast
|
||||
.makeText(context, loadStringRes(Res.string.marmot_group_info_updated), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
shortNotice.show(loadStringRes(Res.string.marmot_group_info_updated))
|
||||
}
|
||||
nav.popBack()
|
||||
} catch (e: Exception) {
|
||||
isSaving = false
|
||||
launch(Dispatchers.Main) {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
loadStringRes(Res.string.marmot_failed_to_update, e.message),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
longNotice.show(loadStringRes(Res.string.marmot_failed_to_update, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -220,7 +220,7 @@ class VoiceReplyViewModel : ViewModel() {
|
||||
loadStringRes(Res.string.upload_error_title),
|
||||
loadStringRes(
|
||||
Res.string.upload_error_voice_message_exception,
|
||||
e.message ?: e.javaClass.simpleName,
|
||||
e.message ?: e::class.simpleName.orEmpty(),
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
|
||||
+3
-7
@@ -29,7 +29,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
|
||||
@@ -39,9 +38,9 @@ import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.observe
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.Gallery
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
|
||||
@Composable
|
||||
@@ -65,7 +64,7 @@ fun RenderLiveActivityBubble(
|
||||
null
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val appPlatform = LocalAppPlatform.current
|
||||
FilledTonalButton(
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 10.dp, bottom = 0.dp, top = 0.dp),
|
||||
onClick = {
|
||||
@@ -75,10 +74,7 @@ fun RenderLiveActivityBubble(
|
||||
val dTag = meetingEvent.address().dTag
|
||||
if (!service.isNullOrBlank() && !endpoint.isNullOrBlank() && dTag.isNotBlank()) {
|
||||
NestBridge.set(accountViewModel)
|
||||
NestActivity.launch(
|
||||
context = context,
|
||||
addressValue = meetingEvent.address().toValue(),
|
||||
)
|
||||
appPlatform.openNestRoom(meetingEvent.address().toValue())
|
||||
} else {
|
||||
// Fall back to the channel route so the user
|
||||
// still lands somewhere — same as a malformed
|
||||
|
||||
+3
-7
@@ -55,7 +55,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -85,11 +84,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpaceClosedFlag
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpaceOpenFlag
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePlannedFlag
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePrivateFlag
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestEditFieldRow
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestNewMessageViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
@@ -292,7 +291,7 @@ private fun OpenNestRoomAction(
|
||||
val roomId = event.address().dTag
|
||||
if (serviceBase.isNullOrBlank() || endpoint.isNullOrBlank() || roomId.isBlank()) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val appPlatform = LocalAppPlatform.current
|
||||
// Filled button so the primary CTA is unmistakable; the actions
|
||||
// Row in the AppBar already centers its children vertically, but
|
||||
// we trim the default end padding so the button sits flush with
|
||||
@@ -300,10 +299,7 @@ private fun OpenNestRoomAction(
|
||||
Button(
|
||||
onClick = {
|
||||
NestBridge.set(accountViewModel)
|
||||
NestActivity.launch(
|
||||
context = context,
|
||||
addressValue = event.address().toValue(),
|
||||
)
|
||||
appPlatform.openNestRoom(event.address().toValue())
|
||||
},
|
||||
contentPadding = ButtonDefaults.ContentPadding,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
|
||||
+15
-13
@@ -20,14 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip52Calendar
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* Serialises NIP-52 calendar events to RFC 5545 iCalendar (`.ics`) text. The output is the
|
||||
@@ -47,8 +44,6 @@ import java.time.format.DateTimeFormatter
|
||||
object IcsExport {
|
||||
private const val PRODID = "-//Amethyst//NIP-52//EN"
|
||||
private const val CRLF = "\r\n"
|
||||
private val UtcStamp: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")
|
||||
private val IsoBasicDate: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd")
|
||||
|
||||
fun appointmentToIcs(
|
||||
event: Any,
|
||||
@@ -189,14 +184,21 @@ object IcsExport {
|
||||
sb.append("CATEGORIES:").append(hashtags.joinToString(",") { escapeText(it) }).append(CRLF)
|
||||
}
|
||||
|
||||
private fun formatUtcInstant(unixSeconds: Long): String = UtcStamp.format(Instant.ofEpochSecond(unixSeconds).atOffset(ZoneOffset.UTC))
|
||||
/** `yyyyMMdd'T'HHmmss'Z'` in UTC. */
|
||||
private fun formatUtcInstant(unixSeconds: Long): String {
|
||||
val date = SearchDate.civilFromDays(unixSeconds.floorDiv(SECONDS_PER_DAY))
|
||||
val secondOfDay = unixSeconds.mod(SECONDS_PER_DAY)
|
||||
return basicDate(date) + "T" + pad2(secondOfDay / 3600) + pad2(secondOfDay / 60 % 60) + pad2(secondOfDay % 60) + "Z"
|
||||
}
|
||||
|
||||
private fun tryFormatBasicDate(iso: String): String? =
|
||||
try {
|
||||
IsoBasicDate.format(LocalDate.parse(iso))
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
/** `yyyy-MM-dd` as `yyyyMMdd`, or null when [iso] is not a valid date. */
|
||||
private fun tryFormatBasicDate(iso: String): String? = SearchDate.parse(iso)?.let(::basicDate)
|
||||
|
||||
private fun basicDate(date: SearchDate): String = date.year.toString().padStart(4, '0') + pad2(date.month.toLong()) + pad2(date.day.toLong())
|
||||
|
||||
private fun pad2(value: Long): String = value.toString().padStart(2, '0')
|
||||
|
||||
private const val SECONDS_PER_DAY = 86_400L
|
||||
|
||||
/**
|
||||
* Escapes text per RFC 5545 §3.3.11: backslash, semicolon, comma, newline. Carriage
|
||||
+6
@@ -111,6 +111,12 @@ interface AppPlatform {
|
||||
@Composable
|
||||
fun rememberWebAppIconModel(url: String): String? = null
|
||||
|
||||
/**
|
||||
* Opens the audio room at [addressValue] (a kind-30312 address) in its own window; callers
|
||||
* hand the account over through `NestBridge` first. Does nothing where rooms are not wired.
|
||||
*/
|
||||
fun openNestRoom(addressValue: String) {}
|
||||
|
||||
/** Draws nothing: previews, and front ends still wiring their pieces. */
|
||||
object None : AppPlatform
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user