refactor: rename Audio Room → Nest project-wide

Aligns class names, package paths, string resource keys, UI text and
intent actions with the Nests branding used by the EGG specs in
nestsClient/specs/. Mechanical rename — no behavior change.

- Folders: audiorooms/ → nests/ (5 paths across amethyst, quartz)
- 30+ class renames (AudioRoom* → Nest*, AudioRooms* → Nests*)
- String resource keys audio_room_* → nest_*
- UI strings "Audio Room"/"Audio Rooms" → "Nest"/"Nests" (incl. all locales)
- Intent extras AUDIO_ROOM_* → NEST_*
- Compose route Route.AudioRooms → Route.Nests

Spec-aligned identifiers (MeetingSpaceEvent, meetingSpaces/, the nip53*
packages) are intentionally untouched — those are NIP-53 protocol
names, not "audio room" branding.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
This commit is contained in:
Claude
2026-04-27 14:36:50 +00:00
parent 208c90b246
commit dc3ac31ae4
74 changed files with 565 additions and 565 deletions
+2 -2
View File
@@ -219,7 +219,7 @@
tools:ignore="DiscouragedApi" />
<activity
android:name=".ui.screen.loggedIn.audiorooms.room.AudioRoomActivity"
android:name=".ui.screen.loggedIn.nests.room.NestActivity"
android:autoRemoveFromRecents="true"
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize|keyboardHidden|keyboard|uiMode|navigation|fontScale|density"
android:supportsPictureInPicture="true"
@@ -271,7 +271,7 @@
android:exported="false" />
<service
android:name=".service.audiorooms.AudioRoomForegroundService"
android:name=".service.nests.NestForegroundService"
android:foregroundServiceType="mediaPlayback|microphone"
android:stopWithTask="true"
android:exported="false" />
@@ -46,7 +46,7 @@ import kotlinx.coroutines.flow.stateIn
* - [saveNestsServersList] — build + sign a new replaceable kind 10112
* event (preserving prior tags' alt etc.)
*
* The list is consumed by `CreateAudioRoomViewModel` to default the
* The list is consumed by `CreateNestViewModel` to default the
* "MoQ service URL" / "MoQ endpoint URL" fields when starting a new
* space, and by the Settings screen for edit / add / remove.
*/
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.audiorooms
package com.vitorpamplona.amethyst.service.nests
import android.app.Notification
import android.app.NotificationChannel
@@ -43,7 +43,7 @@ import com.vitorpamplona.amethyst.ui.MainActivity
*
* Scope decisions:
* - The service does NOT own the MoQ session / decoder / player. Those
* live in `AudioRoomViewModel`. This service exists to keep the process
* live in `NestViewModel`. This service exists to keep the process
* alive while audio is in flight; the VM still drives all wire activity.
* - Foreground type is `mediaPlayback`; if the user starts broadcasting,
* the screen calls [promoteToMicrophone] which re-`startForeground`s
@@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.MainActivity
* full-screen activity). The service uses a shared notification id so
* start/promote/stop is idempotent.
*/
class AudioRoomForegroundService : Service() {
class NestForegroundService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var promoted = false
private var audioFocusRequest: android.media.AudioFocusRequest? = null
@@ -169,25 +169,25 @@ class AudioRoomForegroundService : Service() {
PendingIntent.getService(
this,
1,
Intent(this, AudioRoomForegroundService::class.java).apply { action = ACTION_STOP },
Intent(this, NestForegroundService::class.java).apply { action = ACTION_STOP },
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val title =
if (promoted) {
getString(R.string.audio_room_notification_broadcasting)
getString(R.string.nest_notification_broadcasting)
} else {
getString(R.string.audio_room_notification_listening)
getString(R.string.nest_notification_listening)
}
return NotificationCompat
.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(getString(R.string.audio_room_notification_text))
.setContentText(getString(R.string.nest_notification_text))
.setSmallIcon(R.drawable.amethyst)
.setOngoing(true)
.setContentIntent(openIntent)
.addAction(0, getString(R.string.audio_room_notification_stop), stopIntent)
.addAction(0, getString(R.string.nest_notification_stop), stopIntent)
.setCategory(NotificationCompat.CATEGORY_CALL)
.build()
}
@@ -198,10 +198,10 @@ class AudioRoomForegroundService : Service() {
mgr.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
getString(R.string.audio_room_notification_channel),
getString(R.string.nest_notification_channel),
NotificationManager.IMPORTANCE_LOW,
).apply {
description = getString(R.string.audio_room_notification_channel_description)
description = getString(R.string.nest_notification_channel_description)
setShowBadge(false)
},
)
@@ -218,10 +218,10 @@ class AudioRoomForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
companion object {
private const val CHANNEL_ID = "audio_room_foreground"
private const val CHANNEL_ID = "nest_foreground"
private const val NOTIFICATION_ID = 0xA0D10
private const val ACTION_PROMOTE_TO_MIC = "com.vitorpamplona.amethyst.audio_room.PROMOTE_MIC"
private const val ACTION_STOP = "com.vitorpamplona.amethyst.audio_room.STOP"
private const val ACTION_PROMOTE_TO_MIC = "com.vitorpamplona.amethyst.nest.PROMOTE_MIC"
private const val ACTION_STOP = "com.vitorpamplona.amethyst.nest.STOP"
// 4-hour hard cap — long enough for typical audio-room sessions
// (2-3 h podcasts / panels) plus headroom, short enough to release
@@ -233,7 +233,7 @@ class AudioRoomForegroundService : Service() {
fun startListening(context: Context) {
ContextCompat.startForegroundService(
context,
Intent(context, AudioRoomForegroundService::class.java),
Intent(context, NestForegroundService::class.java),
)
}
@@ -245,7 +245,7 @@ class AudioRoomForegroundService : Service() {
fun promoteToMicrophone(context: Context) {
ContextCompat.startForegroundService(
context,
Intent(context, AudioRoomForegroundService::class.java).apply {
Intent(context, NestForegroundService::class.java).apply {
action = ACTION_PROMOTE_TO_MIC
},
)
@@ -253,7 +253,7 @@ class AudioRoomForegroundService : Service() {
/** Stop and remove the foreground notification. Idempotent. */
fun stop(context: Context) {
context.stopService(Intent(context, AudioRoomForegroundService::class.java))
context.stopService(Intent(context, NestForegroundService::class.java))
}
}
}
@@ -28,11 +28,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentF
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.AudioRoomsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomAdminCommandsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomChatFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomPresenceFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomReactionsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource.BadgesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.profile.datasource.ProfileBadgesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssembler
@@ -50,6 +45,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagF
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomAdminCommandsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomChatFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomPresenceFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomReactionsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource.ProductsFilterAssembler
@@ -110,7 +110,7 @@ class RelaySubscriptionsCoordinator(
val shorts = ShortsFilterAssembler(client)
val publicChats = PublicChatsFilterAssembler(client)
val liveStreams = LiveStreamsFilterAssembler(client)
val audioRooms = AudioRoomsFilterAssembler(client)
val nests = NestsFilterAssembler(client)
val roomPresence = RoomPresenceFilterAssembler(client)
val roomChat = RoomChatFilterAssembler(client)
val roomReactions = RoomReactionsFilterAssembler(client)
@@ -139,7 +139,7 @@ class RelaySubscriptionsCoordinator(
publicChats,
followPacksList,
liveStreams,
audioRooms,
nests,
longs,
articles,
badges,
@@ -67,7 +67,7 @@ object ScrollStateKeys {
const val PUBLIC_CHATS_SCREEN = "PublicChatsFeed"
const val FOLLOW_PACKS_SCREEN = "FollowPacksFeed"
const val LIVE_STREAMS_SCREEN = "LiveStreamsFeed"
const val AUDIO_ROOMS_SCREEN = "AudioRoomsFeed"
const val NESTS_SCREEN = "NestsFeed"
const val LONGS_SCREEN = "LongsFeed"
const val ARTICLES_SCREEN = "ArticlesFeed"
@@ -63,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.ArticlesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.AudioRoomsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.BadgesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.award.AwardBadgeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.profile.ProfileBadgesScreen
@@ -128,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleL
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.LiveStreamsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.LongsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
@@ -245,7 +245,7 @@ fun BuildNavigation(
composableFromEnd<Route.PublicChats> { PublicChatsScreen(accountViewModel, nav) }
composableFromEnd<Route.FollowPacks> { FollowPacksScreen(accountViewModel, nav) }
composableFromEnd<Route.LiveStreams> { LiveStreamsScreen(accountViewModel, nav) }
composableFromEnd<Route.AudioRooms> { AudioRoomsScreen(accountViewModel, nav) }
composableFromEnd<Route.Nests> { NestsScreen(accountViewModel, nav) }
composableFromEnd<Route.Longs> { LongsScreen(accountViewModel, nav) }
composableFromEnd<Route.Articles> { ArticlesScreen(accountViewModel, nav) }
composableFromEnd<Route.NewHlsVideo> { NewHlsVideoScreen(accountViewModel, nav) }
@@ -54,7 +54,7 @@ enum class NavBarItem {
PUBLIC_CHATS,
FOLLOW_PACKS,
LIVE_STREAMS,
AUDIO_ROOMS,
NESTS,
LONGS,
POLLS,
BADGES,
@@ -212,12 +212,12 @@ val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
icon = MaterialSymbols.Sensors,
resolveRoute = { Route.LiveStreams },
),
NavBarItem.AUDIO_ROOMS to
NavBarItem.NESTS to
NavBarItemDef(
id = NavBarItem.AUDIO_ROOMS,
labelRes = R.string.audio_rooms,
id = NavBarItem.NESTS,
labelRes = R.string.nests,
icon = MaterialSymbols.Mic,
resolveRoute = { Route.AudioRooms },
resolveRoute = { Route.Nests },
),
NavBarItem.LONGS to
NavBarItemDef(
@@ -305,7 +305,7 @@ val DrawerFeedsItems: List<NavBarItem> =
NavBarItem.PUBLIC_CHATS,
NavBarItem.FOLLOW_PACKS,
NavBarItem.LIVE_STREAMS,
if (isDebug) NavBarItem.AUDIO_ROOMS else null,
if (isDebug) NavBarItem.NESTS else null,
NavBarItem.LONGS,
NavBarItem.POLLS,
NavBarItem.BADGES,
@@ -89,7 +89,7 @@ sealed class Route {
@Serializable object LiveStreams : Route()
@Serializable object AudioRooms : Route()
@Serializable object Nests : Route()
@Serializable object Longs : Route()
@@ -175,7 +175,7 @@ fun RenderMeetingSpaceEventInner(
ListenToRecordingButton(url = it, accountViewModel = accountViewModel)
}
} else {
com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.JoinAudioRoomButton(
com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.JoinNestButton(
event = noteEvent,
accountViewModel = accountViewModel,
)
@@ -196,7 +196,7 @@ private fun ListenToRecordingButton(
accountViewModel: AccountViewModel,
) {
val context = androidx.compose.ui.platform.LocalContext.current
val noAppMessage = stringRes(R.string.audio_room_no_app_to_open_link)
val noAppMessage = stringRes(R.string.nest_no_app_to_open_link)
androidx.compose.material3.OutlinedButton(onClick = {
val launched =
runCatching {
@@ -208,13 +208,13 @@ private fun ListenToRecordingButton(
}.isSuccess
if (!launched) {
accountViewModel.toastManager.toast(
R.string.audio_room_chat_send_failed_title,
R.string.nest_chat_send_failed_title,
noAppMessage,
user = null,
)
}
}) {
Text(stringRes(R.string.audio_room_listen_to_recording))
Text(stringRes(R.string.nest_listen_to_recording))
}
}
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal.ArticlesFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.dal.AudioRoomsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal.BadgesFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListKnownFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListNewFeedFilter
@@ -48,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.dal.LiveStreamsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.dal.LongsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal.NestsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.OpenPollsState
@@ -101,7 +101,7 @@ class AccountFeedContentStates(
val publicChatsFeed = FeedContentState(PublicChatsFeedFilter(account), scope, LocalCache)
val followPacksFeed = FeedContentState(FollowPacksFeedFilter(account), scope, LocalCache)
val liveStreamsFeed = FeedContentState(LiveStreamsFeedFilter(account), scope, LocalCache)
val audioRoomsFeed = FeedContentState(AudioRoomsFeedFilter(account), scope, LocalCache)
val nestsFeed = FeedContentState(NestsFeedFilter(account), scope, LocalCache)
val longsFeed = FeedContentState(LongsFeedFilter(account), scope, LocalCache)
val articlesFeed = FeedContentState(ArticlesFeedFilter(account), scope, LocalCache)
@@ -168,7 +168,7 @@ class AccountFeedContentStates(
publicChatsFeed.updateFeedWith(newNotes)
followPacksFeed.updateFeedWith(newNotes)
liveStreamsFeed.updateFeedWith(newNotes)
audioRoomsFeed.updateFeedWith(newNotes)
nestsFeed.updateFeedWith(newNotes)
longsFeed.updateFeedWith(newNotes)
articlesFeed.updateFeedWith(newNotes)
@@ -215,7 +215,7 @@ class AccountFeedContentStates(
publicChatsFeed.deleteFromFeed(newNotes)
followPacksFeed.deleteFromFeed(newNotes)
liveStreamsFeed.deleteFromFeed(newNotes)
audioRoomsFeed.deleteFromFeed(newNotes)
nestsFeed.deleteFromFeed(newNotes)
longsFeed.deleteFromFeed(newNotes)
articlesFeed.deleteFromFeed(newNotes)
@@ -1574,7 +1574,7 @@ class AccountViewModel(
callManager.dispose()
com.vitorpamplona.amethyst.service.call.CallSessionBridge
.clear()
com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomBridge
com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestBridge
.clear()
feedStates.destroy()
super.onCleared()
@@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomJoinCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53L
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveStreamTopZappers
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestJoinCard
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.quartz.nip01Core.core.Address
@@ -130,7 +130,7 @@ fun LiveActivityChannelView(
.weight(1f, true),
) {
ShowVideoStreaming(channel, accountViewModel)
AudioRoomJoinCard(channel, accountViewModel)
NestJoinCard(channel, accountViewModel)
LiveStreamTopZappers(channel, accountViewModel, nav)
LiveStreamGoalHeader(channel, accountViewModel, nav)
RefreshingChatroomFeedView(
@@ -38,8 +38,8 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.Gallery
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomBridge
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestBridge
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
@@ -55,7 +55,7 @@ fun RenderLiveActivityBubble(
// LiveActivitiesEvent), so toBestDisplayName() would fall back
// to the truncated bech32. Read the kind-30312 addressable
// directly when the channel's address points to one and use the
// room name + a launch path that goes straight to AudioRoomActivity.
// room name + a launch path that goes straight to NestActivity.
val meetingEvent =
remember(channel.address) {
if (channel.address.kind == MeetingSpaceEvent.KIND) {
@@ -73,8 +73,8 @@ fun RenderLiveActivityBubble(
val endpoint = meetingEvent.endpoint()
val dTag = meetingEvent.address().dTag
if (!service.isNullOrBlank() && !endpoint.isNullOrBlank() && dTag.isNotBlank()) {
AudioRoomBridge.set(accountViewModel)
AudioRoomActivity.launch(
NestBridge.set(accountViewModel)
NestActivity.launch(
context = context,
addressValue = meetingEvent.address().toValue(),
authBaseUrl = service,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
@@ -41,9 +41,9 @@ import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomBridge
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.RenderLiveActivityThumb
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestBridge
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdPadding
@@ -51,7 +51,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEv
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AudioRoomsFeedLoaded(
fun NestsFeedLoaded(
loaded: FeedState.Loaded,
listState: LazyListState,
accountViewModel: AccountViewModel,
@@ -65,7 +65,7 @@ fun AudioRoomsFeedLoaded(
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
Row(Modifier.fillMaxWidth().animateItem()) {
AudioRoomFeedCard(
NestFeedCard(
baseNote = item,
modifier = Modifier.fillMaxWidth(),
accountViewModel = accountViewModel,
@@ -82,12 +82,12 @@ fun AudioRoomsFeedLoaded(
/**
* Audio-rooms list card. Mirrors [RenderLiveActivityThumb] visually but
* routes a tap straight into [AudioRoomActivity] when the underlying event
* routes a tap straight into [NestActivity] when the underlying event
* is a [MeetingSpaceEvent], instead of the thread view that the generic
* `ChannelCardCompose` `ClickableNote` chain would otherwise open.
*/
@Composable
private fun AudioRoomFeedCard(
private fun NestFeedCard(
baseNote: Note,
modifier: Modifier,
accountViewModel: AccountViewModel,
@@ -103,8 +103,8 @@ private fun AudioRoomFeedCard(
val endpoint = meetingEvent.endpoint()
val dTag = meetingEvent.address().dTag
if (!service.isNullOrBlank() && !endpoint.isNullOrBlank() && dTag.isNotBlank()) {
AudioRoomBridge.set(accountViewModel)
AudioRoomActivity.launch(
NestBridge.set(accountViewModel)
NestActivity.launch(
context = context,
addressValue = meetingEvent.address().toValue(),
authBaseUrl = service,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.AlertDialog
@@ -46,32 +46,32 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.create.CreateAudioRoomSheet
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.create.CreateAudioRoomViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.AudioRoomsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.create.CreateNestSheet
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.create.CreateNestViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun AudioRoomsScreen(
fun NestsScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
AudioRoomsScreen(
audioRoomsFeedContentState = accountViewModel.feedStates.audioRoomsFeed,
NestsScreen(
nestsFeedContentState = accountViewModel.feedStates.nestsFeed,
accountViewModel = accountViewModel,
nav = nav,
)
}
@Composable
fun AudioRoomsScreen(
audioRoomsFeedContentState: FeedContentState,
fun NestsScreen(
nestsFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: INav,
) {
WatchLifecycleAndUpdateModel(audioRoomsFeedContentState)
WatchAccountForAudioRoomsScreen(audioRoomsFeedState = audioRoomsFeedContentState, accountViewModel = accountViewModel)
AudioRoomsFilterAssemblerSubscription(accountViewModel)
WatchLifecycleAndUpdateModel(nestsFeedContentState)
WatchAccountForNestsScreen(nestsFeedState = nestsFeedContentState, accountViewModel = accountViewModel)
NestsFilterAssemblerSubscription(accountViewModel)
val nestsServers by accountViewModel.account.nestsServers.flow
.collectAsStateWithLifecycle()
@@ -81,12 +81,12 @@ fun AudioRoomsScreen(
DisappearingScaffold(
isInvertedLayout = false,
topBar = {
AudioRoomsTopBar(accountViewModel, nav)
NestsTopBar(accountViewModel, nav)
},
bottomBar = {
AppBottomBar(Route.AudioRooms, accountViewModel) { route ->
if (route == Route.AudioRooms) {
audioRoomsFeedContentState.sendToTop()
AppBottomBar(Route.Nests, accountViewModel) { route ->
if (route == Route.Nests) {
nestsFeedContentState.sendToTop()
} else {
nav.navBottomBar(route)
}
@@ -105,22 +105,22 @@ fun AudioRoomsScreen(
) {
Icon(
symbol = MaterialSymbols.Add,
contentDescription = stringRes(R.string.audio_room_create_fab),
contentDescription = stringRes(R.string.nest_create_fab),
)
}
},
accountViewModel = accountViewModel,
) {
RefresheableBox(audioRoomsFeedContentState, true) {
SaveableFeedContentState(audioRoomsFeedContentState, scrollStateKey = ScrollStateKeys.AUDIO_ROOMS_SCREEN) { listState ->
RefresheableBox(nestsFeedContentState, true) {
SaveableFeedContentState(nestsFeedContentState, scrollStateKey = ScrollStateKeys.NESTS_SCREEN) { listState ->
RenderFeedContentState(
feedContentState = audioRoomsFeedContentState,
feedContentState = nestsFeedContentState,
accountViewModel = accountViewModel,
listState = listState,
nav = nav,
routeForLastRead = "AudioRoomsFeed",
routeForLastRead = "NestsFeed",
onLoaded = { loaded ->
AudioRoomsFeedLoaded(
NestsFeedLoaded(
loaded = loaded,
listState = listState,
accountViewModel = accountViewModel,
@@ -134,20 +134,20 @@ fun AudioRoomsScreen(
if (showSetupDialog) {
SetUpAudioServerDialog(
defaultUrl = CreateAudioRoomViewModel.DEFAULT_SERVICE_URL,
defaultUrl = CreateNestViewModel.DEFAULT_SERVICE_URL,
onDismiss = { showSetupDialog = false },
onConfirm = {
showSetupDialog = false
accountViewModel.launchSigner {
try {
accountViewModel.account.sendNestsServersList(
listOf(CreateAudioRoomViewModel.DEFAULT_SERVICE_URL),
listOf(CreateNestViewModel.DEFAULT_SERVICE_URL),
)
showCreateSheet = true
} catch (_: Throwable) {
accountViewModel.toastManager.toast(
R.string.audio_rooms,
R.string.audio_room_no_server_save_failed,
R.string.nests,
R.string.nest_no_server_save_failed,
)
}
}
@@ -156,7 +156,7 @@ fun AudioRoomsScreen(
}
if (showCreateSheet) {
CreateAudioRoomSheet(
CreateNestSheet(
accountViewModel = accountViewModel,
onDismiss = { showCreateSheet = false },
)
@@ -171,24 +171,24 @@ private fun SetUpAudioServerDialog(
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringRes(R.string.audio_room_no_server_title)) },
text = { Text(stringRes(R.string.audio_room_no_server_body, defaultUrl)) },
title = { Text(stringRes(R.string.nest_no_server_title)) },
text = { Text(stringRes(R.string.nest_no_server_body, defaultUrl)) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringRes(R.string.audio_room_no_server_use_default))
Text(stringRes(R.string.nest_no_server_use_default))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringRes(R.string.audio_room_no_server_cancel))
Text(stringRes(R.string.nest_no_server_cancel))
}
},
)
}
@Composable
fun WatchAccountForAudioRoomsScreen(
audioRoomsFeedState: FeedContentState,
fun WatchAccountForNestsScreen(
nestsFeedState: FeedContentState,
accountViewModel: AccountViewModel,
) {
val listState by accountViewModel.account.liveLiveStreamsFollowLists.collectAsStateWithLifecycle()
@@ -201,6 +201,6 @@ fun WatchAccountForAudioRoomsScreen(
.collectAsStateWithLifecycle()
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
audioRoomsFeedState.checkKeysInvalidateDataAndSendToTop()
nestsFeedState.checkKeysInvalidateDataAndSendToTop()
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun AudioRoomsTopBar(
fun NestsTopBar(
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -42,7 +42,7 @@ fun AudioRoomsTopBar(
val list by accountViewModel.account.settings.defaultLiveStreamsFollowList
.collectAsStateWithLifecycle()
AudioRoomsTopNavFilterBar(
NestsTopNavFilterBar(
followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list,
accountViewModel = accountViewModel,
@@ -52,7 +52,7 @@ fun AudioRoomsTopBar(
}
@Composable
private fun AudioRoomsTopNavFilterBar(
private fun NestsTopNavFilterBar(
followListsModel: TopNavFilterState,
listName: TopFilter,
accountViewModel: AccountViewModel,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.create
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.create
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -54,32 +54,32 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.AudioRoomsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomBridge
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestBridge
import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.launch
/**
* Bottom sheet that lets the logged-in user start a new NIP-53 kind 30312
* audio room. On submit, [CreateAudioRoomViewModel] builds and signs the
* audio room. On submit, [CreateNestViewModel] builds and signs the
* MeetingSpaceEvent (with the user as `host`), broadcasts it to the
* account's relays, and then launches [AudioRoomActivity] against the
* account's relays, and then launches [NestActivity] against the
* fresh address the user lands inside the room as host with the
* Talk button enabled.
*
* Hidden by default; surfaced by the "Start space" FAB on
* [AudioRoomsScreen].
* [NestsScreen].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateAudioRoomSheet(
fun CreateNestSheet(
accountViewModel: AccountViewModel,
onDismiss: () -> Unit,
) {
val viewModelKey = remember(accountViewModel) { accountViewModel.account.userProfile().pubkeyHex }
val viewModel: CreateAudioRoomViewModel =
viewModel(key = "CreateAudioRoom-$viewModelKey")
val viewModel: CreateNestViewModel =
viewModel(key = "CreateNest-$viewModelKey")
LaunchedEffect(viewModel) { viewModel.bindAccountIfMissing(accountViewModel) }
val state by viewModel.state.collectAsState()
@@ -96,14 +96,14 @@ fun CreateAudioRoomSheet(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringRes(R.string.audio_room_create_title),
text = stringRes(R.string.nest_create_title),
style = MaterialTheme.typography.titleLarge,
)
OutlinedTextField(
value = state.roomName,
onValueChange = viewModel::onRoomNameChange,
label = { Text(stringRes(R.string.audio_room_create_field_room)) },
label = { Text(stringRes(R.string.nest_create_field_room)) },
singleLine = true,
isError = state.error != null && state.roomName.isBlank(),
modifier = Modifier.fillMaxWidth(),
@@ -112,7 +112,7 @@ fun CreateAudioRoomSheet(
OutlinedTextField(
value = state.summary,
onValueChange = viewModel::onSummaryChange,
label = { Text(stringRes(R.string.audio_room_create_field_summary)) },
label = { Text(stringRes(R.string.nest_create_field_summary)) },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
)
@@ -120,27 +120,27 @@ fun CreateAudioRoomSheet(
OutlinedTextField(
value = state.serviceUrl,
onValueChange = viewModel::onServiceUrlChange,
label = { Text(stringRes(R.string.audio_room_create_field_service)) },
label = { Text(stringRes(R.string.nest_create_field_service)) },
singleLine = true,
isError = state.error != null && state.serviceUrl.isBlank(),
supportingText = { Text(stringRes(R.string.audio_room_create_field_service_hint)) },
supportingText = { Text(stringRes(R.string.nest_create_field_service_hint)) },
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.endpointUrl,
onValueChange = viewModel::onEndpointUrlChange,
label = { Text(stringRes(R.string.audio_room_create_field_endpoint)) },
label = { Text(stringRes(R.string.nest_create_field_endpoint)) },
singleLine = true,
isError = state.error != null && state.endpointUrl.isBlank(),
supportingText = { Text(stringRes(R.string.audio_room_create_field_endpoint_hint)) },
supportingText = { Text(stringRes(R.string.nest_create_field_endpoint_hint)) },
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.imageUrl,
onValueChange = viewModel::onImageUrlChange,
label = { Text(stringRes(R.string.audio_room_create_field_image)) },
label = { Text(stringRes(R.string.nest_create_field_image)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
@@ -157,7 +157,7 @@ fun CreateAudioRoomSheet(
onCheckedChange = viewModel::onScheduledToggle,
)
Spacer(Modifier.width(8.dp))
Text(stringRes(R.string.audio_room_create_schedule_toggle))
Text(stringRes(R.string.nest_create_schedule_toggle))
}
if (state.scheduled) {
ScheduleStartPicker(
@@ -181,7 +181,7 @@ fun CreateAudioRoomSheet(
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = onDismiss, enabled = !state.isPublishing) {
Text(stringRes(R.string.audio_room_create_cancel))
Text(stringRes(R.string.nest_create_cancel))
}
Spacer(Modifier.width(8.dp))
Button(
@@ -189,10 +189,10 @@ fun CreateAudioRoomSheet(
scope.launch {
val launchInfo = viewModel.publishAndBuildLaunchInfo() ?: return@launch
// Hand the active AccountViewModel to the
// separately-tasked AudioRoomActivity (mirrors
// separately-tasked NestActivity (mirrors
// the join-card flow).
AudioRoomBridge.set(accountViewModel)
AudioRoomActivity.launch(
NestBridge.set(accountViewModel)
NestActivity.launch(
context = context,
addressValue = launchInfo.addressValue,
authBaseUrl = launchInfo.authBaseUrl,
@@ -212,7 +212,7 @@ fun CreateAudioRoomSheet(
strokeWidth = 2.dp,
)
} else {
Text(stringRes(R.string.audio_room_create_submit))
Text(stringRes(R.string.nest_create_submit))
}
}
}
@@ -238,7 +238,7 @@ private fun ScheduleStartPicker(
val pretty =
if (unixSeconds <= 0L) {
stringRes(R.string.audio_room_create_when)
stringRes(R.string.nest_create_when)
} else {
val instant = java.util.Date(unixSeconds * 1000L)
java.text.DateFormat
@@ -302,7 +302,7 @@ private fun ScheduleStartPicker(
resetPickersToCommitted()
showDate = false
}) {
Text(stringRes(R.string.audio_room_create_cancel))
Text(stringRes(R.string.nest_create_cancel))
}
},
) {
@@ -312,7 +312,7 @@ private fun ScheduleStartPicker(
if (showTime) {
androidx.compose.material3.TimePickerDialog(
title = { Text(stringRes(R.string.audio_room_create_when)) },
title = { Text(stringRes(R.string.nest_create_when)) },
onDismissRequest = {
resetPickersToCommitted()
showTime = false
@@ -340,14 +340,14 @@ private fun ScheduleStartPicker(
onChange(localSeconds - offsetSec)
}
showTime = false
}) { Text(stringRes(R.string.audio_room_create_submit)) }
}) { Text(stringRes(R.string.nest_create_submit)) }
},
dismissButton = {
TextButton(onClick = {
resetPickersToCommitted()
showTime = false
}) {
Text(stringRes(R.string.audio_room_create_cancel))
Text(stringRes(R.string.nest_create_cancel))
}
},
) {
@@ -18,11 +18,11 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.create
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.create
import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.NestActivity
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.endpoint
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.image
@@ -37,16 +37,16 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
/**
* Backing ViewModel for [CreateAudioRoomSheet]. Holds form state, runs
* Backing ViewModel for [CreateNestSheet]. Holds form state, runs
* [publishAndBuildLaunchInfo] which signs and broadcasts a NIP-53 kind
* 30312 [MeetingSpaceEvent] tagging the user as `host`, then returns
* the launch parameters for [AudioRoomActivity].
* the launch parameters for [NestActivity].
*
* The defaults point at `nostrnests.com`'s public moq-rs deployment so a
* blank form produces a working room. The user can edit them to point at
* their own moq-rs / moq-auth pair.
*/
class CreateAudioRoomViewModel : ViewModel() {
class CreateNestViewModel : ViewModel() {
/** Lazily bound on first composition; the sheet calls [bindAccountIfMissing]. */
@Volatile private var account: AccountViewModel? = null
@@ -91,7 +91,7 @@ class CreateAudioRoomViewModel : ViewModel() {
/**
* Build the kind-30312 event, sign + broadcast it, and return the
* launch info the sheet needs to start [AudioRoomActivity]. Returns
* launch info the sheet needs to start [NestActivity]. Returns
* null on validation or network failure (with [FormState.error]
* set so the UI can render it).
*/
@@ -222,7 +222,7 @@ class CreateAudioRoomViewModel : ViewModel() {
}
/**
* Captured fields needed to launch [AudioRoomActivity]. Mirrors the
* Captured fields needed to launch [NestActivity]. Mirrors the
* `EXTRA_*` set the activity expects; pulled out here so the sheet
* is a thin renderer.
*/
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
@@ -45,7 +45,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag
* to MeetingSpaceEvent (30312) and MeetingRoomEvent (30313) so that the
* Clubhouse-style audio-room surface is independent of video live streams.
*/
class AudioRoomsFeedFilter(
class NestsFeedFilter(
val account: Account,
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
@@ -27,19 +27,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
class AudioRoomsQueryState(
class NestsQueryState(
val account: Account,
val feedStates: AccountFeedContentStates,
val scope: CoroutineScope,
)
@Stable
class AudioRoomsFilterAssembler(
class NestsFilterAssembler(
client: INostrClient,
) : ComposeSubscriptionManager<AudioRoomsQueryState>() {
) : ComposeSubscriptionManager<NestsQueryState>() {
val group =
listOf(
AudioRoomsSubAssembler(client, ::allKeys),
NestsSubAssembler(client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -27,21 +27,21 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourc
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun AudioRoomsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
AudioRoomsFilterAssemblerSubscription(
accountViewModel.dataSources().audioRooms,
fun NestsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
NestsFilterAssemblerSubscription(
accountViewModel.dataSources().nests,
accountViewModel,
)
}
@Composable
fun AudioRoomsFilterAssemblerSubscription(
dataSource: AudioRoomsFilterAssembler,
fun NestsFilterAssemblerSubscription(
dataSource: NestsFilterAssembler,
accountViewModel: AccountViewModel,
) {
val state =
remember(accountViewModel.account) {
AudioRoomsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
NestsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.User
@@ -41,34 +41,34 @@ import kotlinx.coroutines.launch
* sharing the wire filter avoids duplicate REQs on relays when both the Live
* Streams and Audio Rooms screens are open for the same user.
*/
class AudioRoomsSubAssembler(
class NestsSubAssembler(
client: INostrClient,
allKeys: () -> Set<AudioRoomsQueryState>,
) : PerUserAndFollowListEoseManager<AudioRoomsQueryState, TopFilter>(client, allKeys) {
allKeys: () -> Set<NestsQueryState>,
) : PerUserAndFollowListEoseManager<NestsQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: AudioRoomsQueryState,
key: NestsQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeLiveActivitiesFilter(feedSettings, since, key.feedStates.audioRoomsFeed.lastNoteCreatedAtIfFilled())
return makeLiveActivitiesFilter(feedSettings, since, key.feedStates.nestsFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: AudioRoomsQueryState) = key.account.userProfile()
override fun user(key: NestsQueryState) = key.account.userProfile()
override fun list(key: AudioRoomsQueryState) = key.listName()
override fun list(key: NestsQueryState) = key.listName()
fun AudioRoomsQueryState.listNameFlow() = account.settings.defaultLiveStreamsFollowList
fun NestsQueryState.listNameFlow() = account.settings.defaultLiveStreamsFollowList
fun AudioRoomsQueryState.listName() = listNameFlow().value
fun NestsQueryState.listName() = listNameFlow().value
fun AudioRoomsQueryState.followsPerRelayFlow() = account.liveLiveStreamsFollowListsPerRelay
fun NestsQueryState.followsPerRelayFlow() = account.liveLiveStreamsFollowListsPerRelay
fun AudioRoomsQueryState.followsPerRelay() = followsPerRelayFlow().value
fun NestsQueryState.followsPerRelay() = followsPerRelayFlow().value
private val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: AudioRoomsQueryState): Subscription {
override fun newSub(key: NestsQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
@@ -84,7 +84,7 @@ class AudioRoomsSubAssembler(
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.audioRoomsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
key.feedStates.nestsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.experimental.audiorooms.admin.AdminCommandEvent
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -49,7 +49,7 @@ class RoomChatQueryState(
* REQs `kinds=[1311], #a=[roomATag]` against the user's outbox
* relays so the chat panel sees every message tagged for this
* room. Events arrive in [com.vitorpamplona.amethyst.model.LocalCache];
* the room screen pipes them into `AudioRoomViewModel.onChatEvent`.
* the room screen pipes them into `NestViewModel.onChatEvent`.
*/
class RoomChatFilterSubAssembler(
client: INostrClient,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
@@ -47,7 +47,7 @@ class RoomPresenceQueryState(
* already talks to. The events arrive in [com.vitorpamplona.amethyst.model.LocalCache]
* via the normal client pipeline; the room screen observes them
* with `LocalCache.observeEvents(...)` and feeds each event into
* `AudioRoomViewModel.onPresenceEvent`.
* `NestViewModel.onPresenceEvent`.
*
* One assembler instance services every audio-room screen the
* `key` (= room a-tag) keeps overlapping screens (in unlikely
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -63,18 +63,18 @@ import kotlinx.coroutines.launch
* original), or closes the room with a destructive button.
*
* Visibility gating (host-only) is the caller's job;
* [EditAudioRoomViewModel] preserves every promoted participant
* [EditNestViewModel] preserves every promoted participant
* verbatim, so a save MUST NOT silently demote anyone.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditAudioRoomSheet(
fun EditNestSheet(
accountViewModel: AccountViewModel,
event: MeetingSpaceEvent,
onDismiss: () -> Unit,
) {
val key = remember(event) { "EditAudioRoom-${event.dTag()}" }
val viewModel: EditAudioRoomViewModel = viewModel(key = key)
val key = remember(event) { "EditNest-${event.dTag()}" }
val viewModel: EditNestViewModel = viewModel(key = key)
LaunchedEffect(viewModel, event) { viewModel.bind(accountViewModel, event) }
val state by viewModel.state.collectAsState()
@@ -85,8 +85,8 @@ fun EditAudioRoomSheet(
if (confirmCloseOpen) {
androidx.compose.material3.AlertDialog(
onDismissRequest = { confirmCloseOpen = false },
title = { Text(stringRes(R.string.audio_room_close_room_confirm_title)) },
text = { Text(stringRes(R.string.audio_room_close_room_confirm_body)) },
title = { Text(stringRes(R.string.nest_close_room_confirm_title)) },
text = { Text(stringRes(R.string.nest_close_room_confirm_body)) },
confirmButton = {
TextButton(
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
@@ -97,12 +97,12 @@ fun EditAudioRoomSheet(
}
},
) {
Text(stringRes(R.string.audio_room_close_room_confirm_action))
Text(stringRes(R.string.nest_close_room_confirm_action))
}
},
dismissButton = {
TextButton(onClick = { confirmCloseOpen = false }) {
Text(stringRes(R.string.audio_room_create_cancel))
Text(stringRes(R.string.nest_create_cancel))
}
},
)
@@ -113,41 +113,41 @@ fun EditAudioRoomSheet(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringRes(R.string.audio_room_edit_title),
text = stringRes(R.string.nest_edit_title),
style = MaterialTheme.typography.titleLarge,
)
OutlinedTextField(
value = state.roomName,
onValueChange = viewModel::setRoomName,
label = { Text(stringRes(R.string.audio_room_create_field_room)) },
label = { Text(stringRes(R.string.nest_create_field_room)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.summary,
onValueChange = viewModel::setSummary,
label = { Text(stringRes(R.string.audio_room_create_field_summary)) },
label = { Text(stringRes(R.string.nest_create_field_summary)) },
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.serviceUrl,
onValueChange = viewModel::setServiceUrl,
label = { Text(stringRes(R.string.audio_room_create_field_service)) },
label = { Text(stringRes(R.string.nest_create_field_service)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.endpointUrl,
onValueChange = viewModel::setEndpointUrl,
label = { Text(stringRes(R.string.audio_room_create_field_endpoint)) },
label = { Text(stringRes(R.string.nest_create_field_endpoint)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.imageUrl,
onValueChange = viewModel::setImageUrl,
label = { Text(stringRes(R.string.audio_room_create_field_image)) },
label = { Text(stringRes(R.string.nest_create_field_image)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
@@ -171,11 +171,11 @@ fun EditAudioRoomSheet(
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
onClick = { confirmCloseOpen = true },
) {
Text(stringRes(R.string.audio_room_close_action))
Text(stringRes(R.string.nest_close_action))
}
Row {
TextButton(onClick = onDismiss, enabled = !state.isPublishing) {
Text(stringRes(R.string.audio_room_create_cancel))
Text(stringRes(R.string.nest_create_cancel))
}
Spacer(Modifier.width(8.dp))
Button(
@@ -189,7 +189,7 @@ fun EditAudioRoomSheet(
if (state.isPublishing) {
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
} else {
Text(stringRes(R.string.audio_room_edit_save))
Text(stringRes(R.string.nest_edit_save))
}
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
/**
* Backing ViewModel for [EditAudioRoomSheet]. Loads the existing
* Backing ViewModel for [EditNestSheet]. Loads the existing
* [MeetingSpaceEvent], lets the host edit room name / summary /
* image / endpoint, then republishes kind-30312 with the SAME `d`
* tag so the relay treats it as a replacement of the original.
@@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.update
* speakers or admins who were promoted in a previous version of
* the event (audit risk note in the Tier-1 plan).
*/
class EditAudioRoomViewModel : ViewModel() {
class EditNestViewModel : ViewModel() {
@Volatile private var account: AccountViewModel? = null
@Volatile private var original: MeetingSpaceEvent? = null
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -38,7 +38,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -62,7 +62,7 @@ import kotlinx.coroutines.launch
@Composable
internal fun HandRaiseQueueSection(
event: MeetingSpaceEvent,
viewModel: AudioRoomViewModel,
viewModel: NestViewModel,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
) {
@@ -83,7 +83,7 @@ internal fun HandRaiseQueueSection(
val scope = rememberCoroutineScope()
Column(modifier = modifier.fillMaxWidth().padding(top = 12.dp)) {
Text(
text = stringRes(R.string.audio_room_hand_raise_queue_title),
text = stringRes(R.string.nest_hand_raise_queue_title),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -112,7 +112,7 @@ internal fun HandRaiseQueueSection(
}
},
) {
Text(stringRes(R.string.audio_room_hand_raise_approve))
Text(stringRes(R.string.nest_hand_raise_approve))
}
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import android.app.PendingIntent
import android.app.PictureInPictureParams
@@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.asSharedFlow
/**
* Standalone activity that owns the lifetime of an audio-room session. The
* lobby ([AudioRoomJoinCard]) launches this activity when the user taps
* lobby ([NestJoinCard]) launches this activity when the user taps
* "Join audio room"; finishing it tears down the MoQ session, the
* broadcaster (if any), and the foreground notification.
*
@@ -52,11 +52,11 @@ import kotlinx.coroutines.flow.asSharedFlow
* keep the speakers visible while doing other things. Mute / leave are
* exposed via the system PIP overlay as [RemoteAction]s.
*
* AccountViewModel arrives via [AudioRoomBridge] same pattern as
* AccountViewModel arrives via [NestBridge] same pattern as
* [com.vitorpamplona.amethyst.ui.call.CallActivity] uses for
* `CallSessionBridge`.
*/
class AudioRoomActivity : AppCompatActivity() {
class NestActivity : AppCompatActivity() {
private val isInPipMode = mutableStateOf(false)
private val isMuted = mutableStateOf(false)
private val isConnected = mutableStateOf(false)
@@ -96,7 +96,7 @@ class AudioRoomActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val accountViewModel = AudioRoomBridge.accountViewModel
val accountViewModel = NestBridge.accountViewModel
val addressValue = intent.getStringExtra(EXTRA_ADDRESS)
val authBaseUrl = intent.getStringExtra(EXTRA_AUTH_BASE_URL)
val endpoint = intent.getStringExtra(EXTRA_ENDPOINT)
@@ -145,7 +145,7 @@ class AudioRoomActivity : AppCompatActivity() {
setContent {
AmethystTheme {
AudioRoomActivityContent(
NestActivityContent(
addressValue = addressValue,
room =
com.vitorpamplona.nestsclient.NestsRoomConfig(
@@ -211,7 +211,7 @@ class AudioRoomActivity : AppCompatActivity() {
PictureInPictureParams
.Builder()
// Landscape ratio — the PIP layout is a horizontal row of
// avatars under a title (see AudioRoomPipScreen), which 9:16
// avatars under a title (see NestPipScreen), which 9:16
// squeezed into a sliver. 16:9 fits the row and the room name.
.setAspectRatio(Rational(16, 9))
.setActions(buildPipActions())
@@ -233,13 +233,13 @@ class AudioRoomActivity : AppCompatActivity() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val muteIconRes = if (isMuted.value) R.drawable.ic_mic_off else R.drawable.ic_mic_on
val muteLabel = getString(if (isMuted.value) R.string.audio_room_unmute else R.string.audio_room_mute)
val muteLabel = getString(if (isMuted.value) R.string.nest_unmute else R.string.nest_mute)
return listOf(
RemoteAction(Icon.createWithResource(this, muteIconRes), muteLabel, muteLabel, muteIntent),
RemoteAction(
Icon.createWithResource(this, R.drawable.ic_call_end),
getString(R.string.audio_room_leave),
getString(R.string.audio_room_leave),
getString(R.string.nest_leave),
getString(R.string.nest_leave),
leaveIntent,
),
)
@@ -251,14 +251,14 @@ class AudioRoomActivity : AppCompatActivity() {
}
companion object {
const val EXTRA_ADDRESS = "com.vitorpamplona.amethyst.AUDIO_ROOM_ADDRESS"
const val EXTRA_AUTH_BASE_URL = "com.vitorpamplona.amethyst.AUDIO_ROOM_AUTH_BASE_URL"
const val EXTRA_ENDPOINT = "com.vitorpamplona.amethyst.AUDIO_ROOM_ENDPOINT"
const val EXTRA_HOST_PUBKEY = "com.vitorpamplona.amethyst.AUDIO_ROOM_HOST_PUBKEY"
const val EXTRA_ROOM_ID = "com.vitorpamplona.amethyst.AUDIO_ROOM_ROOM_ID"
const val EXTRA_KIND = "com.vitorpamplona.amethyst.AUDIO_ROOM_KIND"
private const val ACTION_PIP_TOGGLE_MUTE = "com.vitorpamplona.amethyst.AUDIO_ROOM_PIP_MUTE"
private const val ACTION_PIP_LEAVE = "com.vitorpamplona.amethyst.AUDIO_ROOM_PIP_LEAVE"
const val EXTRA_ADDRESS = "com.vitorpamplona.amethyst.NEST_ADDRESS"
const val EXTRA_AUTH_BASE_URL = "com.vitorpamplona.amethyst.NEST_AUTH_BASE_URL"
const val EXTRA_ENDPOINT = "com.vitorpamplona.amethyst.NEST_ENDPOINT"
const val EXTRA_HOST_PUBKEY = "com.vitorpamplona.amethyst.NEST_HOST_PUBKEY"
const val EXTRA_ROOM_ID = "com.vitorpamplona.amethyst.NEST_ROOM_ID"
const val EXTRA_KIND = "com.vitorpamplona.amethyst.NEST_KIND"
private const val ACTION_PIP_TOGGLE_MUTE = "com.vitorpamplona.amethyst.NEST_PIP_MUTE"
private const val ACTION_PIP_LEAVE = "com.vitorpamplona.amethyst.NEST_PIP_LEAVE"
private const val PIP_MUTE_REQ = 0x6A001
private const val PIP_LEAVE_REQ = 0x6A002
@@ -272,7 +272,7 @@ class AudioRoomActivity : AppCompatActivity() {
kind: Int,
) {
context.startActivity(
Intent(context, AudioRoomActivity::class.java).apply {
Intent(context, NestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_ADDRESS, addressValue)
putExtra(EXTRA_AUTH_BASE_URL, authBaseUrl)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -32,18 +32,18 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.BroadcastUiState
import com.vitorpamplona.amethyst.commons.viewmodels.ConnectionUiState
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.audiorooms.AudioRoomForegroundService
import com.vitorpamplona.amethyst.service.nests.NestForegroundService
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomAdminCommandsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomChatFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomPresenceFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.datasource.RoomReactionsFilterAssemblerSubscription
import com.vitorpamplona.quartz.experimental.audiorooms.admin.AdminCommandEvent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomAdminCommandsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomChatFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomPresenceFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.RoomReactionsFilterAssemblerSubscription
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
@@ -58,16 +58,16 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
* Top-level composable for [AudioRoomActivity]. Observes the room event +
* Top-level composable for [NestActivity]. Observes the room event +
* VM, auto-connects on entry, runs the kind-10312 presence loop, drives
* the foreground service, and flips between full-screen and PIP layouts.
*
* The [AudioRoomActivity] supplies [isInPipMode] (a Compose state observed
* The [NestActivity] supplies [isInPipMode] (a Compose state observed
* via the activity's `mutableStateOf`) and [onLeave] which finishes the
* activity. Everything else is local to this composable's scope.
*/
@Composable
internal fun AudioRoomActivityContent(
internal fun NestActivityContent(
addressValue: String,
room: com.vitorpamplona.nestsclient.NestsRoomConfig,
accountViewModel: AccountViewModel,
@@ -88,7 +88,7 @@ internal fun AudioRoomActivityContent(
LoadAddressableNote(parsedAddress, accountViewModel) { addressableNote ->
addressableNote ?: return@LoadAddressableNote
val event = addressableNote.event as? MeetingSpaceEvent ?: return@LoadAddressableNote
AudioRoomActivityBody(
NestActivityBody(
event = event,
roomNote = addressableNote,
room = room,
@@ -103,7 +103,7 @@ internal fun AudioRoomActivityContent(
}
@Composable
private fun AudioRoomActivityBody(
private fun NestActivityBody(
event: MeetingSpaceEvent,
roomNote: com.vitorpamplona.amethyst.model.AddressableNote,
room: com.vitorpamplona.nestsclient.NestsRoomConfig,
@@ -127,12 +127,12 @@ private fun AudioRoomActivityBody(
val account = accountViewModel.account
val signer = account.signer
val viewModel: AudioRoomViewModel =
val viewModel: NestViewModel =
viewModel(
key = "${room.authBaseUrl}|${room.roomId}",
factory =
remember(room, signer) {
AudioRoomViewModelFactory(
NestViewModelFactory(
signer = signer,
room = room,
)
@@ -343,22 +343,22 @@ private fun AudioRoomActivityBody(
val isBroadcasting = ui.broadcast is BroadcastUiState.Broadcasting
LaunchedEffect(isLive, isBroadcasting) {
when {
isLive && isBroadcasting -> AudioRoomForegroundService.promoteToMicrophone(context)
isLive -> AudioRoomForegroundService.startListening(context)
else -> AudioRoomForegroundService.stop(context)
isLive && isBroadcasting -> NestForegroundService.promoteToMicrophone(context)
isLive -> NestForegroundService.startListening(context)
else -> NestForegroundService.stop(context)
}
}
DisposableEffect(Unit) { onDispose { AudioRoomForegroundService.stop(context) } }
DisposableEffect(Unit) { onDispose { NestForegroundService.stop(context) } }
if (isInPipMode) {
AudioRoomPipScreen(
NestPipScreen(
title = event.room(),
onStage = onStage,
ui = ui,
accountViewModel = accountViewModel,
)
} else {
AudioRoomFullScreen(
NestFullScreen(
event = event,
roomNote = roomNote,
onStage = onStage,
@@ -18,21 +18,21 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
/**
* Process-level singleton that hands the active [AccountViewModel] from the
* main activity to [AudioRoomActivity]. Mirrors the
* main activity to [NestActivity]. Mirrors the
* [com.vitorpamplona.amethyst.service.call.CallSessionBridge] pattern so a
* separately-tasked Activity in the same process can reach the logged-in
* account without serialising a `NostrSigner` through an Intent extra.
*
* Set immediately before `startActivity(Intent(..., AudioRoomActivity::class))`
* Set immediately before `startActivity(Intent(..., NestActivity::class))`
* is called and cleared on logout / account switch.
*/
object AudioRoomBridge {
object NestBridge {
@Volatile
var accountViewModel: AccountViewModel? = null
private set
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -49,7 +49,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -63,15 +63,15 @@ import kotlinx.coroutines.launch
/**
* Live-chat panel (T1 #2) renders the kind-1311 transcript for
* the room and provides a send field. Messages flow from
* [AudioRoomViewModel.chat] (populated by the LocalCache observer
* in [AudioRoomActivityContent]); send routes through
* [NestViewModel.chat] (populated by the LocalCache observer
* in [NestActivityContent]); send routes through
* `account.signAndComputeBroadcast` directly so the VM stays free
* of platform / signing dependencies.
*/
@Composable
internal fun AudioRoomChatPanel(
internal fun NestChatPanel(
roomATag: ATag,
viewModel: AudioRoomViewModel,
viewModel: NestViewModel,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
) {
@@ -100,7 +100,7 @@ internal fun AudioRoomChatPanel(
Column(modifier = modifier.fillMaxWidth()) {
if (messages.isEmpty()) {
Text(
text = stringRes(R.string.audio_room_chat_empty),
text = stringRes(R.string.nest_chat_empty),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 12.dp),
@@ -178,7 +178,7 @@ private fun ChatComposer(
value = draft,
onValueChange = { draft = it },
modifier = Modifier.weight(1f),
placeholder = { Text(stringRes(R.string.audio_room_chat_placeholder)) },
placeholder = { Text(stringRes(R.string.nest_chat_placeholder)) },
singleLine = false,
maxLines = 4,
enabled = !isSending,
@@ -212,7 +212,7 @@ private fun ChatComposer(
?: result.exceptionOrNull()?.let { it::class.simpleName }
?: "unknown error"
accountViewModel.toastManager.toast(
R.string.audio_room_chat_send_failed_title,
R.string.nest_chat_send_failed_title,
why,
user = null,
)
@@ -220,7 +220,7 @@ private fun ChatComposer(
}
},
) {
Text(stringRes(R.string.audio_room_chat_send))
Text(stringRes(R.string.nest_chat_send))
}
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
@@ -99,7 +99,7 @@ internal fun StagePeopleRow(
@Composable
internal fun connectingLabel(connection: ConnectionUiState.Connecting): String =
when (connection.step) {
ConnectionUiState.Step.ResolvingRoom -> stringRes(R.string.audio_room_connecting_resolving)
ConnectionUiState.Step.OpeningTransport -> stringRes(R.string.audio_room_connecting_transport)
ConnectionUiState.Step.MoqHandshake -> stringRes(R.string.audio_room_connecting_handshake)
ConnectionUiState.Step.ResolvingRoom -> stringRes(R.string.nest_connecting_resolving)
ConnectionUiState.Step.OpeningTransport -> stringRes(R.string.nest_connecting_transport)
ConnectionUiState.Step.MoqHandshake -> stringRes(R.string.nest_connecting_handshake)
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import android.Manifest
import android.content.pm.PackageManager
@@ -58,10 +58,10 @@ import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomUiState
import com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.BroadcastUiState
import com.vitorpamplona.amethyst.commons.viewmodels.ConnectionUiState
import com.vitorpamplona.amethyst.commons.viewmodels.NestUiState
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.RoomTheme
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -73,28 +73,28 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTa
import kotlinx.coroutines.launch
/**
* Full-screen layout for [AudioRoomActivity]. Renders title + summary,
* Full-screen layout for [NestActivity]. Renders title + summary,
* host/speaker/audience rows (with active-speaker rings), connection chip
* + mute, talk row (when allowed), hand-raise, and a Leave button that
* finishes the activity.
*
* The PIP variant lives in [AudioRoomPipScreen]; the activity flips
* The PIP variant lives in [NestPipScreen]; the activity flips
* between them based on `isInPipMode`.
*/
@Composable
internal fun AudioRoomFullScreen(
internal fun NestFullScreen(
event: MeetingSpaceEvent,
roomNote: com.vitorpamplona.amethyst.model.AddressableNote,
onStage: List<ParticipantTag>,
viewModel: AudioRoomViewModel,
ui: AudioRoomUiState,
viewModel: NestViewModel,
ui: NestUiState,
accountViewModel: AccountViewModel,
handRaised: Boolean,
onHandRaisedChange: (Boolean) -> Unit,
onLeave: () -> Unit,
) {
val roomTheme = androidx.compose.runtime.remember(event) { RoomTheme.from(event) }
AudioRoomThemedScope(theme = roomTheme, accountViewModel = accountViewModel) {
NestThemedScope(theme = roomTheme, accountViewModel = accountViewModel) {
Column(
modifier =
Modifier
@@ -126,7 +126,7 @@ internal fun AudioRoomFullScreen(
androidx.compose.material3.IconButton(onClick = { showHostMenu = true }) {
Icon(
symbol = MaterialSymbols.MoreVert,
contentDescription = stringRes(R.string.audio_room_overflow_menu),
contentDescription = stringRes(R.string.nest_overflow_menu),
)
}
val context = androidx.compose.ui.platform.LocalContext.current
@@ -135,7 +135,7 @@ internal fun AudioRoomFullScreen(
onDismissRequest = { showHostMenu = false },
) {
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringRes(R.string.audio_room_share_action)) },
text = { Text(stringRes(R.string.nest_share_action)) },
onClick = {
showHostMenu = false
shareRoomNaddr(context, event)
@@ -143,7 +143,7 @@ internal fun AudioRoomFullScreen(
)
if (isHost) {
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringRes(R.string.audio_room_edit_title)) },
text = { Text(stringRes(R.string.nest_edit_title)) },
onClick = {
showHostMenu = false
showEditSheet = true
@@ -154,7 +154,7 @@ internal fun AudioRoomFullScreen(
}
}
if (showEditSheet) {
EditAudioRoomSheet(
EditNestSheet(
accountViewModel = accountViewModel,
event = event,
onDismiss = { showEditSheet = false },
@@ -178,7 +178,7 @@ internal fun AudioRoomFullScreen(
Text(
text =
androidx.compose.ui.res.pluralStringResource(
R.plurals.audio_room_listener_count,
R.plurals.nest_listener_count,
listenerCount,
listenerCount,
),
@@ -211,8 +211,8 @@ internal fun AudioRoomFullScreen(
grid = participantGrid,
speakingNow = ui.speakingNow,
accountViewModel = accountViewModel,
onStageLabel = stringRes(R.string.audio_room_stage),
audienceLabel = stringRes(R.string.audio_room_audience),
onStageLabel = stringRes(R.string.nest_stage),
audienceLabel = stringRes(R.string.nest_audience),
reactionsByPubkey = reactionsByPubkey,
connectingSpeakers = ui.connectingSpeakers,
onLongPressParticipant = onLongPressParticipant,
@@ -257,12 +257,12 @@ internal fun AudioRoomFullScreen(
symbol = MaterialSymbols.PanTool,
contentDescription =
stringRes(
if (handRaised) R.string.audio_room_lower_hand else R.string.audio_room_raise_hand,
if (handRaised) R.string.nest_lower_hand else R.string.nest_raise_hand,
),
)
}
OutlinedButton(onClick = { showReactionPicker = true }) {
Text(stringRes(R.string.audio_room_reactions_button))
Text(stringRes(R.string.nest_reactions_button))
}
OutlinedButton(
onClick = {
@@ -273,7 +273,7 @@ internal fun AudioRoomFullScreen(
}
},
) {
Text(stringRes(R.string.audio_room_leave))
Text(stringRes(R.string.nest_leave))
}
}
if (showReactionPicker) {
@@ -288,8 +288,8 @@ internal fun AudioRoomFullScreen(
if (showHostLeaveConfirm) {
AlertDialog(
onDismissRequest = { showHostLeaveConfirm = false },
title = { Text(stringRes(R.string.audio_room_leave_host_title)) },
text = { Text(stringRes(R.string.audio_room_leave_host_body)) },
title = { Text(stringRes(R.string.nest_leave_host_title)) },
text = { Text(stringRes(R.string.nest_leave_host_body)) },
confirmButton = {
TextButton(
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
@@ -299,15 +299,15 @@ internal fun AudioRoomFullScreen(
val ok = closeMeetingSpace(accountViewModel, event)
if (!ok) {
accountViewModel.toastManager.toast(
R.string.audio_rooms,
R.string.audio_room_leave_host_close_failed,
R.string.nests,
R.string.nest_leave_host_close_failed,
)
}
onLeave()
}
},
) {
Text(stringRes(R.string.audio_room_leave_host_close))
Text(stringRes(R.string.nest_leave_host_close))
}
},
dismissButton = {
@@ -317,13 +317,13 @@ internal fun AudioRoomFullScreen(
onLeave()
},
) {
Text(stringRes(R.string.audio_room_leave_host_just_leave))
Text(stringRes(R.string.nest_leave_host_just_leave))
}
},
)
}
AudioRoomChatPanel(
NestChatPanel(
roomATag =
ATag(
kind = event.kind,
@@ -341,8 +341,8 @@ internal fun AudioRoomFullScreen(
@Composable
private fun ConnectionRow(
viewModel: AudioRoomViewModel,
ui: AudioRoomUiState,
viewModel: NestViewModel,
ui: NestUiState,
) {
Row(
modifier = Modifier.fillMaxWidth().padding(top = 12.dp),
@@ -352,7 +352,7 @@ private fun ConnectionRow(
when (val connection = ui.connection) {
is ConnectionUiState.Idle, is ConnectionUiState.Closed -> {
Button(onClick = { viewModel.connect() }) {
Text(stringRes(R.string.audio_room_connect))
Text(stringRes(R.string.nest_connect))
}
}
@@ -373,7 +373,7 @@ private fun ConnectionRow(
AssistChip(
onClick = {},
enabled = false,
label = { Text(stringRes(R.string.audio_room_reconnecting)) },
label = { Text(stringRes(R.string.nest_reconnecting)) },
)
}
@@ -381,7 +381,7 @@ private fun ConnectionRow(
AssistChip(
onClick = {},
enabled = false,
label = { Text(stringRes(R.string.audio_room_connected)) },
label = { Text(stringRes(R.string.nest_connected)) },
colors =
AssistChipDefaults.assistChipColors(
disabledLabelColor = MaterialTheme.colorScheme.primary,
@@ -395,7 +395,7 @@ private fun ConnectionRow(
symbol = if (ui.isMuted) MaterialSymbols.AutoMirrored.VolumeOff else MaterialSymbols.AutoMirrored.VolumeUp,
contentDescription =
stringRes(
if (ui.isMuted) R.string.audio_room_unmute else R.string.audio_room_mute,
if (ui.isMuted) R.string.nest_unmute else R.string.nest_mute,
),
)
}
@@ -403,13 +403,13 @@ private fun ConnectionRow(
is ConnectionUiState.Failed -> {
Text(
text = stringRes(R.string.audio_room_audio_failed, connection.reason),
text = stringRes(R.string.nest_audio_failed, connection.reason),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.weight(1f, fill = false),
)
Button(onClick = { viewModel.connect() }) {
Text(stringRes(R.string.audio_room_connect))
Text(stringRes(R.string.nest_connect))
}
}
}
@@ -418,8 +418,8 @@ private fun ConnectionRow(
@Composable
private fun TalkRow(
viewModel: AudioRoomViewModel,
ui: AudioRoomUiState,
viewModel: NestViewModel,
ui: NestUiState,
speakerPubkeyHex: String,
) {
// Render whenever the user can act on broadcast state. The
@@ -472,11 +472,11 @@ private fun TalkRow(
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
}) {
Text(stringRes(R.string.audio_room_talk))
Text(stringRes(R.string.nest_talk))
}
if (showDenialWarning) {
Text(
text = stringRes(R.string.audio_room_record_permission_required),
text = stringRes(R.string.nest_record_permission_required),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
@@ -497,7 +497,7 @@ private fun TalkRow(
)
}
}) {
Text(stringRes(R.string.audio_room_open_settings))
Text(stringRes(R.string.nest_open_settings))
}
}
}
@@ -506,7 +506,7 @@ private fun TalkRow(
AssistChip(
onClick = {},
enabled = false,
label = { Text(stringRes(R.string.audio_room_broadcast_connecting)) },
label = { Text(stringRes(R.string.nest_broadcast_connecting)) },
)
}
@@ -514,7 +514,7 @@ private fun TalkRow(
AssistChip(
onClick = {},
enabled = false,
label = { Text(stringRes(R.string.audio_room_broadcasting)) },
label = { Text(stringRes(R.string.nest_broadcasting)) },
colors =
AssistChipDefaults.assistChipColors(
disabledLabelColor = MaterialTheme.colorScheme.error,
@@ -528,24 +528,24 @@ private fun TalkRow(
symbol = if (broadcast.isMuted) MaterialSymbols.AutoMirrored.VolumeOff else MaterialSymbols.AutoMirrored.VolumeUp,
contentDescription =
stringRes(
if (broadcast.isMuted) R.string.audio_room_mic_unmute else R.string.audio_room_mic_mute,
if (broadcast.isMuted) R.string.nest_mic_unmute else R.string.nest_mic_mute,
),
)
}
OutlinedButton(onClick = { viewModel.stopBroadcast() }) {
Text(stringRes(R.string.audio_room_stop_talking))
Text(stringRes(R.string.nest_stop_talking))
}
}
is BroadcastUiState.Failed -> {
Text(
text = stringRes(R.string.audio_room_broadcast_failed, broadcast.reason),
text = stringRes(R.string.nest_broadcast_failed, broadcast.reason),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.weight(1f, fill = false),
)
Button(onClick = { viewModel.startBroadcast(speakerPubkeyHex) }) {
Text(stringRes(R.string.audio_room_talk))
Text(stringRes(R.string.nest_talk))
}
}
}
@@ -557,9 +557,9 @@ private fun TalkRow(
* other field verbatim (room name, summary, image, service, endpoint,
* participants). Returns true on success.
*
* Reuses [EditAudioRoomViewModel.buildEditTemplate] so the close-on-leave
* Reuses [EditNestViewModel.buildEditTemplate] so the close-on-leave
* path emits the same shape as the explicit Edit Close room action.
* Decoupled from [EditAudioRoomViewModel] state so the host's unsaved
* Decoupled from [EditNestViewModel] state so the host's unsaved
* edits in the Edit sheet (if any) cannot accidentally leak into the
* close payload.
*/
@@ -568,7 +568,7 @@ private suspend fun closeMeetingSpace(
event: MeetingSpaceEvent,
): Boolean {
val verbatim =
EditAudioRoomViewModel.FormState(
EditNestViewModel.FormState(
dTag = event.dTag(),
roomName = event.room().orEmpty(),
summary = event.summary().orEmpty(),
@@ -580,7 +580,7 @@ private suspend fun closeMeetingSpace(
)
return try {
val template =
EditAudioRoomViewModel.buildEditTemplate(event, verbatim, StatusTag.STATUS.CLOSED)
EditNestViewModel.buildEditTemplate(event, verbatim, StatusTag.STATUS.CLOSED)
accountViewModel.account.signAndComputeBroadcast(template)
true
} catch (_: Throwable) {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -47,7 +47,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEv
/**
* Lobby card rendered in [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ChannelView]
* for NIP-53 kind 30312 meeting-space events. Shows the room title +
* summary + a "Join audio room" button that launches [AudioRoomActivity].
* summary + a "Join audio room" button that launches [NestActivity].
*
* The lobby is intentionally thin: no audio session, no presence event, no
* hand-raise. All of that lives behind the Join button so the on-the-wire
@@ -57,19 +57,19 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEv
* non-nests servers we can't connect to.
*/
@Composable
fun AudioRoomJoinCard(
fun NestJoinCard(
baseChannel: LiveActivitiesChannel,
accountViewModel: AccountViewModel,
) {
LoadAddressableNote(baseChannel.address, accountViewModel) { addressableNote ->
addressableNote ?: return@LoadAddressableNote
val event = addressableNote.event as? MeetingSpaceEvent ?: return@LoadAddressableNote
AudioRoomJoinCardContent(event, accountViewModel)
NestJoinCardContent(event, accountViewModel)
}
}
@Composable
private fun AudioRoomJoinCardContent(
private fun NestJoinCardContent(
event: MeetingSpaceEvent,
accountViewModel: AccountViewModel,
) {
@@ -107,7 +107,7 @@ private fun AudioRoomJoinCardContent(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
JoinAudioRoomButton(event = event, accountViewModel = accountViewModel)
JoinNestButton(event = event, accountViewModel = accountViewModel)
}
}
}
@@ -123,7 +123,7 @@ private fun AudioRoomJoinCardContent(
* the audio plane.
*/
@Composable
fun JoinAudioRoomButton(
fun JoinNestButton(
event: MeetingSpaceEvent,
accountViewModel: AccountViewModel,
) {
@@ -138,8 +138,8 @@ fun JoinAudioRoomButton(
val kind = event.kind
Button(onClick = {
AudioRoomBridge.set(accountViewModel)
AudioRoomActivity.launch(
NestBridge.set(accountViewModel)
NestActivity.launch(
context = context,
addressValue = addressValue,
authBaseUrl = serviceBase,
@@ -149,6 +149,6 @@ fun JoinAudioRoomButton(
kind = kind,
)
}) {
Text(stringRes(R.string.audio_room_join))
Text(stringRes(R.string.nest_join))
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -36,7 +36,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomUiState
import com.vitorpamplona.amethyst.commons.viewmodels.NestUiState
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size35dp
@@ -46,13 +46,13 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTa
* Compact PIP layout. Shows up to four "on stage" speakers with a ring on
* whoever is actively delivering audio, plus the room title. The mute /
* leave actions live in the system PIP overlay as RemoteActions see
* [AudioRoomActivity.buildPipActions].
* [NestActivity.buildPipActions].
*/
@Composable
internal fun AudioRoomPipScreen(
internal fun NestPipScreen(
title: String?,
onStage: List<ParticipantTag>,
ui: AudioRoomUiState,
ui: NestUiState,
accountViewModel: AccountViewModel,
) {
val ringColor = MaterialTheme.colorScheme.primary
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -55,7 +55,7 @@ import kotlinx.coroutines.withContext
* passthrough Box.
*/
@Composable
internal fun AudioRoomThemedScope(
internal fun NestThemedScope(
theme: RoomTheme,
accountViewModel: AccountViewModel,
content: @Composable () -> Unit,
@@ -18,11 +18,11 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
import com.vitorpamplona.nestsclient.NestsRoomConfig
import com.vitorpamplona.nestsclient.OkHttpNestsClient
import com.vitorpamplona.nestsclient.audio.AudioRecordCapture
@@ -33,19 +33,19 @@ import com.vitorpamplona.nestsclient.transport.QuicWebTransportFactory
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
/**
* Android-side Factory for [AudioRoomViewModel]. The ViewModel itself lives
* Android-side Factory for [NestViewModel]. The ViewModel itself lives
* in `commons/` so a future desktop port can reuse the orchestration once
* Compose Desktop has WebTransport; this factory binds it to the Android
* actuals (OkHttp HTTP, pure-Kotlin QUIC, MediaCodec Opus, AudioTrack +
* AudioRecord on the speaker side).
*/
internal class AudioRoomViewModelFactory(
internal class NestViewModelFactory(
private val signer: NostrSigner,
private val room: NestsRoomConfig,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
AudioRoomViewModel(
NestViewModel(
httpClient = OkHttpNestsClient(),
transport = QuicWebTransportFactory(),
decoderFactory = { MediaCodecOpusDecoder() },
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -49,7 +49,7 @@ import com.vitorpamplona.amethyst.ui.note.ZapCustomDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.experimental.audiorooms.admin.AdminCommandEvent
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
@@ -134,7 +134,7 @@ internal fun ParticipantHostActionsSheet(
)
}
Spacer(Modifier.height(8.dp))
// View Profile — AudioRoomActivity is a separate Android
// View Profile — NestActivity is a separate Android
// Activity without its own nav stack, so the deep-link
// path goes through MainActivity. Launching `nostr:npub1...`
// via ACTION_VIEW lets the existing MainActivity URI
@@ -145,8 +145,8 @@ internal fun ParticipantHostActionsSheet(
// audio-room foreground service keeps audio alive while
// the user is on the profile screen.
val context = LocalContext.current
val noAppMessage = stringRes(R.string.audio_room_no_app_to_open_link)
ActionRow(stringRes(R.string.audio_room_participant_view_profile)) {
val noAppMessage = stringRes(R.string.nest_no_app_to_open_link)
ActionRow(stringRes(R.string.nest_participant_view_profile)) {
val npub = NPub.create(target)
val launched =
runCatching {
@@ -165,7 +165,7 @@ internal fun ParticipantHostActionsSheet(
}.isSuccess
if (!launched) {
accountViewModel.toastManager.toast(
R.string.audio_room_chat_send_failed_title,
R.string.nest_chat_send_failed_title,
noAppMessage,
user = null,
)
@@ -180,7 +180,7 @@ internal fun ParticipantHostActionsSheet(
// the profile screen rather than wired into a non-nav-host
// ModalBottomSheet.
var showZapDialog by rememberSaveable { mutableStateOf(false) }
ActionRow(stringRes(R.string.audio_room_participant_zap)) {
ActionRow(stringRes(R.string.nest_participant_zap)) {
showZapDialog = true
}
if (showZapDialog) {
@@ -191,7 +191,7 @@ internal fun ParticipantHostActionsSheet(
}
// Pre-resolved at composition because callbacks below run
// outside a @Composable scope (stringRes is composable-only).
val splitUnsupportedMsg = stringRes(R.string.audio_room_participant_zap_split_unsupported)
val splitUnsupportedMsg = stringRes(R.string.nest_participant_zap_split_unsupported)
ZapCustomDialog(
onZapStarts = {},
onClose = {
@@ -229,9 +229,9 @@ internal fun ParticipantHostActionsSheet(
text =
stringRes(
if (isFollowing) {
R.string.audio_room_participant_unfollow
R.string.nest_participant_unfollow
} else {
R.string.audio_room_participant_follow
R.string.nest_participant_follow
},
),
) {
@@ -242,9 +242,9 @@ internal fun ParticipantHostActionsSheet(
text =
stringRes(
if (isHidden) {
R.string.audio_room_participant_unmute
R.string.nest_participant_unmute
} else {
R.string.audio_room_participant_mute
R.string.nest_participant_mute
},
),
) {
@@ -255,16 +255,16 @@ internal fun ParticipantHostActionsSheet(
// Host-only rows.
if (isLocalUserHost && target != event.pubKey) {
Spacer(Modifier.height(4.dp))
ActionRow(stringRes(R.string.audio_room_promote_speaker)) {
ActionRow(stringRes(R.string.nest_promote_speaker)) {
broadcast(RoomParticipantActions.setRole(event, target, ROLE.SPEAKER))
onDismiss()
}
ActionRow(stringRes(R.string.audio_room_demote_listener)) {
ActionRow(stringRes(R.string.nest_demote_listener)) {
broadcast(RoomParticipantActions.demoteToListener(event, target))
onDismiss()
}
ActionRow(
text = stringRes(R.string.audio_room_kick_action),
text = stringRes(R.string.nest_kick_action),
color = MaterialTheme.colorScheme.error,
) {
broadcast(AdminCommandEvent.kick(roomATag, target))
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import android.content.Context
import androidx.compose.ui.text.font.Font
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
@@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE
*
* Extracted out of the screen Composable so they can be unit-tested
* without an AccountViewModel / signer (the sign+broadcast path is
* the only side effect, mirroring [EditAudioRoomViewModel.buildEditTemplate]).
* the only side effect, mirroring [EditNestViewModel.buildEditTemplate]).
*
* Risk-mitigation: each builder reads ALL participants from
* [original] and rebuilds the list with one row mutated; never
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -66,7 +66,7 @@ internal fun RoomReactionPickerSheet(
) {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
Text(
text = stringRes(R.string.audio_room_reactions_title),
text = stringRes(R.string.nest_reactions_title),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.RoomReaction
*
* Hides itself when there are no reactions in the window the
* 30-s sliding-window aggregator in
* [com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomViewModel.recentReactions]
* [com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel.recentReactions]
* drops stale entries on the 1-s tick.
*/
@Composable
@@ -456,11 +456,11 @@
<string name="public_chats">Veřejné chaty</string>
<string name="follow_packs">Seznamy sledování</string>
<string name="live_streams">Živé vysílání</string>
<string name="audio_rooms">Audio místnosti</string>
<string name="audio_room_stage">Pódium</string>
<string name="audio_room_audience">Publikum</string>
<string name="audio_room_raise_hand">Přihlásit se</string>
<string name="audio_room_lower_hand">Dát ruku dolů</string>
<string name="nests">Audio místnosti</string>
<string name="nest_stage">Pódium</string>
<string name="nest_audience">Publikum</string>
<string name="nest_raise_hand">Přihlásit se</string>
<string name="nest_lower_hand">Dát ruku dolů</string>
<string name="longs">Videa</string>
<string name="articles">Články</string>
<string name="private_bookmarks">Soukromé záložky</string>
@@ -462,11 +462,11 @@ anz der Bedingungen ist erforderlich</string>
<string name="public_chats">Öffentliche Chats</string>
<string name="follow_packs">Follow-Pakete</string>
<string name="live_streams">Live-Streams</string>
<string name="audio_rooms">Audioräume</string>
<string name="audio_room_stage">Bühne</string>
<string name="audio_room_audience">Publikum</string>
<string name="audio_room_raise_hand">Hand heben</string>
<string name="audio_room_lower_hand">Hand senken</string>
<string name="nests">Audioräume</string>
<string name="nest_stage">Bühne</string>
<string name="nest_audience">Publikum</string>
<string name="nest_raise_hand">Hand heben</string>
<string name="nest_lower_hand">Hand senken</string>
<string name="longs">Videos</string>
<string name="articles">Artikel</string>
<string name="private_bookmarks">Private Lesezeichen</string>
@@ -458,11 +458,11 @@
<string name="public_chats">सार्वजनिक चर्चा</string>
<string name="follow_packs">अनुचरण पोटलियाँ</string>
<string name="live_streams">सीधा प्रसारण</string>
<string name="audio_rooms">ध्वनि शाला</string>
<string name="audio_room_stage">मंच</string>
<string name="audio_room_audience">श्रोतागण</string>
<string name="audio_room_raise_hand">हाथ उठाएँ</string>
<string name="audio_room_lower_hand">हाथ नीचे करें</string>
<string name="nests">ध्वनि शाला</string>
<string name="nest_stage">मंच</string>
<string name="nest_audience">श्रोतागण</string>
<string name="nest_raise_hand">हाथ उठाएँ</string>
<string name="nest_lower_hand">हाथ नीचे करें</string>
<string name="longs">चलचित्र</string>
<string name="articles">लेख</string>
<string name="private_bookmarks">निजी स्मर्तव्य</string>
@@ -458,11 +458,11 @@
<string name="public_chats">Nyilvános csevegések</string>
<string name="follow_packs">Követési csomagok</string>
<string name="live_streams">Élő közvetítések</string>
<string name="audio_rooms">Hangszobák</string>
<string name="audio_room_stage">Állapot</string>
<string name="audio_room_audience">Közönség</string>
<string name="audio_room_raise_hand">Kéz felemelése</string>
<string name="audio_room_lower_hand">Kéz leengedése</string>
<string name="nests">Hangszobák</string>
<string name="nest_stage">Állapot</string>
<string name="nest_audience">Közönség</string>
<string name="nest_raise_hand">Kéz felemelése</string>
<string name="nest_lower_hand">Kéz leengedése</string>
<string name="longs">Videók</string>
<string name="articles">Cikkek</string>
<string name="private_bookmarks">Privát könyvjelzők</string>
@@ -455,11 +455,11 @@
<string name="public_chats">Czaty publiczne</string>
<string name="follow_packs">Pakiety subskrybentów</string>
<string name="live_streams">Transmisje na żywo</string>
<string name="audio_rooms">Pokoje audio</string>
<string name="audio_room_stage">Scena</string>
<string name="audio_room_audience">Publiczność</string>
<string name="audio_room_raise_hand">Podnieś rękę</string>
<string name="audio_room_lower_hand">Opuść rękę</string>
<string name="nests">Pokoje audio</string>
<string name="nest_stage">Scena</string>
<string name="nest_audience">Publiczność</string>
<string name="nest_raise_hand">Podnieś rękę</string>
<string name="nest_lower_hand">Opuść rękę</string>
<string name="longs">Filmy wideo</string>
<string name="articles">Artykuły</string>
<string name="private_bookmarks">Prywatne Zakładki</string>
@@ -456,11 +456,11 @@
<string name="public_chats">Chats públicos</string>
<string name="follow_packs">Pacotes de seguidos</string>
<string name="live_streams">Transmissões ao vivo</string>
<string name="audio_rooms">Salas de áudio</string>
<string name="audio_room_stage">Palco</string>
<string name="audio_room_audience">Plateia</string>
<string name="audio_room_raise_hand">Levantar mão</string>
<string name="audio_room_lower_hand">Abaixar mão</string>
<string name="nests">Salas de áudio</string>
<string name="nest_stage">Palco</string>
<string name="nest_audience">Plateia</string>
<string name="nest_raise_hand">Levantar mão</string>
<string name="nest_lower_hand">Abaixar mão</string>
<string name="longs">Vídeos</string>
<string name="articles">Artigos</string>
<string name="private_bookmarks">Itens Salvos Privados</string>
@@ -469,11 +469,11 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="public_chats">Javni klepeti</string>
<string name="follow_packs">Paketi sledenih</string>
<string name="live_streams">Posnetki v živo</string>
<string name="audio_rooms">Zvočne sobe</string>
<string name="audio_room_stage">Prizorišče</string>
<string name="audio_room_audience">Občinstvo</string>
<string name="audio_room_raise_hand">Dvigni roko</string>
<string name="audio_room_lower_hand">Spusti roko</string>
<string name="nests">Zvočne sobe</string>
<string name="nest_stage">Prizorišče</string>
<string name="nest_audience">Občinstvo</string>
<string name="nest_raise_hand">Dvigni roko</string>
<string name="nest_lower_hand">Spusti roko</string>
<string name="longs">Videoposnetki</string>
<string name="articles">Članki</string>
<string name="private_bookmarks">Privatni zaznamki</string>
@@ -456,11 +456,11 @@
<string name="public_chats">Offentliga chattar</string>
<string name="follow_packs">Följpaket</string>
<string name="live_streams">Livesändningar</string>
<string name="audio_rooms">Ljudrum</string>
<string name="audio_room_stage">Scen</string>
<string name="audio_room_audience">Publik</string>
<string name="audio_room_raise_hand">Räck upp handen</string>
<string name="audio_room_lower_hand">Sänk handen</string>
<string name="nests">Ljudrum</string>
<string name="nest_stage">Scen</string>
<string name="nest_audience">Publik</string>
<string name="nest_raise_hand">Räck upp handen</string>
<string name="nest_lower_hand">Sänk handen</string>
<string name="longs">Videor</string>
<string name="articles">Artiklar</string>
<string name="private_bookmarks">Privata Bokmärken</string>
@@ -458,11 +458,11 @@
<string name="public_chats">公共聊天</string>
<string name="follow_packs">关注包</string>
<string name="live_streams">直播</string>
<string name="audio_rooms">音频室</string>
<string name="audio_room_stage">阶段</string>
<string name="audio_room_audience">观众</string>
<string name="audio_room_raise_hand">举手</string>
<string name="audio_room_lower_hand">放下手</string>
<string name="nests">音频室</string>
<string name="nest_stage">阶段</string>
<string name="nest_audience">观众</string>
<string name="nest_raise_hand">举手</string>
<string name="nest_lower_hand">放下手</string>
<string name="longs">视频</string>
<string name="articles">文章</string>
<string name="private_bookmarks">私人书签</string>
+92 -92
View File
@@ -488,107 +488,107 @@
<string name="public_chats">Public Chats</string>
<string name="follow_packs">Follow Packs</string>
<string name="live_streams">Live Streams</string>
<string name="audio_rooms">Audio Rooms</string>
<string name="audio_room_stage">Stage</string>
<string name="audio_room_audience">Audience</string>
<string name="audio_room_raise_hand">Raise hand</string>
<string name="audio_room_lower_hand">Lower hand</string>
<string name="audio_room_connect">Connect audio</string>
<string name="audio_room_disconnect">Disconnect</string>
<string name="audio_room_mute">Mute</string>
<string name="audio_room_unmute">Unmute</string>
<string name="audio_room_connecting">Connecting…</string>
<string name="audio_room_connecting_resolving">Resolving room</string>
<string name="audio_room_connecting_transport">Opening transport</string>
<string name="audio_room_connecting_handshake">Negotiating audio</string>
<string name="audio_room_connected">Audio connected</string>
<string name="audio_room_reconnecting">Reconnecting…</string>
<string name="audio_room_listen_to_recording">Listen to recording</string>
<string name="audio_room_chat_send_failed_title">Couldn\'t send message</string>
<string name="audio_room_no_app_to_open_link">No app installed to open this link.</string>
<string name="audio_room_close_room_confirm_title">Close this room?</string>
<string name="audio_room_close_room_confirm_body">All attendees will be disconnected. The room will show as CLOSED in the feed.</string>
<string name="audio_room_close_room_confirm_action">Close room</string>
<string name="audio_room_create_when_in_past">Pick a future start time.</string>
<string name="audio_room_audio_failed">Audio failed: %1$s</string>
<string name="audio_room_audio_unavailable">Audio is not available for this room</string>
<string name="audio_room_talk">Talk</string>
<string name="audio_room_stop_talking">Stop talking</string>
<string name="audio_room_mic_mute">Mute mic</string>
<string name="audio_room_mic_unmute">Unmute mic</string>
<string name="audio_room_broadcast_connecting">Going live…</string>
<string name="audio_room_broadcasting">Live</string>
<string name="audio_room_broadcast_failed">Broadcast failed: %1$s</string>
<string name="audio_room_record_permission_required">Microphone access is required to talk in this room.</string>
<string name="audio_room_open_settings">Open settings</string>
<string name="audio_room_notification_channel">Audio rooms</string>
<string name="audio_room_notification_channel_description">Keeps audio playing while a room is open.</string>
<string name="audio_room_notification_listening">Audio room connected</string>
<string name="audio_room_notification_broadcasting">Audio room — Live</string>
<string name="audio_room_notification_text">Tap to return.</string>
<string name="audio_room_notification_stop">Stop</string>
<string name="audio_room_join">Join audio room</string>
<string name="audio_room_leave">Leave</string>
<string name="audio_room_leave_host_title">End this audio room?</string>
<string name="audio_room_leave_host_body">You\'re the host. Closing the room will disconnect everyone. Choose \"Just leave\" if you want to come back later — the room will auto-close after 8 hours of inactivity.</string>
<string name="audio_room_leave_host_close">Close room</string>
<string name="audio_room_leave_host_just_leave">Just leave</string>
<string name="audio_room_leave_host_close_failed">Couldn\'t mark room as closed. Leaving anyway — room will auto-close.</string>
<plurals name="audio_room_listener_count">
<string name="nests">Nests</string>
<string name="nest_stage">Stage</string>
<string name="nest_audience">Audience</string>
<string name="nest_raise_hand">Raise hand</string>
<string name="nest_lower_hand">Lower hand</string>
<string name="nest_connect">Connect audio</string>
<string name="nest_disconnect">Disconnect</string>
<string name="nest_mute">Mute</string>
<string name="nest_unmute">Unmute</string>
<string name="nest_connecting">Connecting…</string>
<string name="nest_connecting_resolving">Resolving room</string>
<string name="nest_connecting_transport">Opening transport</string>
<string name="nest_connecting_handshake">Negotiating audio</string>
<string name="nest_connected">Audio connected</string>
<string name="nest_reconnecting">Reconnecting…</string>
<string name="nest_listen_to_recording">Listen to recording</string>
<string name="nest_chat_send_failed_title">Couldn\'t send message</string>
<string name="nest_no_app_to_open_link">No app installed to open this link.</string>
<string name="nest_close_room_confirm_title">Close this room?</string>
<string name="nest_close_room_confirm_body">All attendees will be disconnected. The room will show as CLOSED in the feed.</string>
<string name="nest_close_room_confirm_action">Close room</string>
<string name="nest_create_when_in_past">Pick a future start time.</string>
<string name="nest_audio_failed">Audio failed: %1$s</string>
<string name="nest_audio_unavailable">Audio is not available for this room</string>
<string name="nest_talk">Talk</string>
<string name="nest_stop_talking">Stop talking</string>
<string name="nest_mic_mute">Mute mic</string>
<string name="nest_mic_unmute">Unmute mic</string>
<string name="nest_broadcast_connecting">Going live…</string>
<string name="nest_broadcasting">Live</string>
<string name="nest_broadcast_failed">Broadcast failed: %1$s</string>
<string name="nest_record_permission_required">Microphone access is required to talk in this room.</string>
<string name="nest_open_settings">Open settings</string>
<string name="nest_notification_channel">Nests</string>
<string name="nest_notification_channel_description">Keeps audio playing while a room is open.</string>
<string name="nest_notification_listening">Nest connected</string>
<string name="nest_notification_broadcasting">Nest — Live</string>
<string name="nest_notification_text">Tap to return.</string>
<string name="nest_notification_stop">Stop</string>
<string name="nest_join">Join nest</string>
<string name="nest_leave">Leave</string>
<string name="nest_leave_host_title">End this nest?</string>
<string name="nest_leave_host_body">You\'re the host. Closing the room will disconnect everyone. Choose \"Just leave\" if you want to come back later — the room will auto-close after 8 hours of inactivity.</string>
<string name="nest_leave_host_close">Close room</string>
<string name="nest_leave_host_just_leave">Just leave</string>
<string name="nest_leave_host_close_failed">Couldn\'t mark room as closed. Leaving anyway — room will auto-close.</string>
<plurals name="nest_listener_count">
<item quantity="one">%1$d listener</item>
<item quantity="other">%1$d listeners</item>
</plurals>
<string name="audio_room_chat_send">Send</string>
<string name="audio_room_chat_placeholder">Say something…</string>
<string name="audio_room_chat_empty">No messages yet. Be the first to chat.</string>
<string name="audio_room_reactions_title">React</string>
<string name="audio_room_reactions_button">React</string>
<string name="audio_room_edit_title">Edit room</string>
<string name="audio_room_edit_save">Save</string>
<string name="audio_room_close_action">Close room</string>
<string name="audio_room_overflow_menu">Room actions</string>
<string name="audio_room_hand_raise_queue_title">Raised hands</string>
<string name="audio_room_hand_raise_approve">Approve</string>
<string name="audio_room_promote_speaker">Promote to speaker</string>
<string name="audio_room_demote_listener">Demote to listener</string>
<string name="audio_room_kick_action">Kick</string>
<string name="audio_room_participant_view_profile">View profile</string>
<string name="audio_room_participant_zap">Send zap</string>
<string name="audio_room_participant_zap_split_unsupported">Split zaps are not supported from inside an audio room. Open the profile screen to send.</string>
<string name="audio_room_participant_follow">Follow</string>
<string name="audio_room_participant_unfollow">Unfollow</string>
<string name="audio_room_participant_mute">Mute</string>
<string name="audio_room_participant_unmute">Unmute</string>
<string name="audio_room_share_action">Share room</string>
<string name="audio_room_create_fab">Start space</string>
<string name="audio_room_no_server_title">Set up an audio server</string>
<string name="audio_room_no_server_body">You haven\'t picked an audio server yet. Add %1$s to your server list and continue?\n\nYou can change this later in Settings.</string>
<string name="audio_room_no_server_use_default">Use default</string>
<string name="audio_room_no_server_cancel">Cancel</string>
<string name="audio_room_no_server_save_failed">Couldn\'t save your audio server list. Try again.</string>
<string name="audio_room_create_title">Start a new audio room</string>
<string name="audio_room_create_field_room">Room name</string>
<string name="audio_room_create_field_summary">What\'s it about?</string>
<string name="audio_room_create_field_service">MoQ service URL</string>
<string name="audio_room_create_field_service_hint">Auth sidecar — defaults to nostrnests.com</string>
<string name="audio_room_create_field_endpoint">MoQ relay endpoint</string>
<string name="audio_room_create_field_endpoint_hint">WebTransport URL — usually the same host</string>
<string name="audio_room_create_field_image">Cover image URL (optional)</string>
<string name="audio_room_create_cancel">Cancel</string>
<string name="audio_room_create_submit">Start space</string>
<string name="audio_room_create_schedule_toggle">Schedule for later</string>
<string name="audio_room_create_when">Pick a start time</string>
<string name="nests_servers_title">Audio-room servers</string>
<string name="nests_servers_explainer">Choose which MoQ host servers Amethyst publishes your audio rooms to. The first entry is used by default when you start a new space.</string>
<string name="nest_chat_send">Send</string>
<string name="nest_chat_placeholder">Say something…</string>
<string name="nest_chat_empty">No messages yet. Be the first to chat.</string>
<string name="nest_reactions_title">React</string>
<string name="nest_reactions_button">React</string>
<string name="nest_edit_title">Edit room</string>
<string name="nest_edit_save">Save</string>
<string name="nest_close_action">Close room</string>
<string name="nest_overflow_menu">Room actions</string>
<string name="nest_hand_raise_queue_title">Raised hands</string>
<string name="nest_hand_raise_approve">Approve</string>
<string name="nest_promote_speaker">Promote to speaker</string>
<string name="nest_demote_listener">Demote to listener</string>
<string name="nest_kick_action">Kick</string>
<string name="nest_participant_view_profile">View profile</string>
<string name="nest_participant_zap">Send zap</string>
<string name="nest_participant_zap_split_unsupported">Split zaps are not supported from inside a nest. Open the profile screen to send.</string>
<string name="nest_participant_follow">Follow</string>
<string name="nest_participant_unfollow">Unfollow</string>
<string name="nest_participant_mute">Mute</string>
<string name="nest_participant_unmute">Unmute</string>
<string name="nest_share_action">Share room</string>
<string name="nest_create_fab">Start space</string>
<string name="nest_no_server_title">Set up a nest server</string>
<string name="nest_no_server_body">You haven\'t picked a nest server yet. Add %1$s to your server list and continue?\n\nYou can change this later in Settings.</string>
<string name="nest_no_server_use_default">Use default</string>
<string name="nest_no_server_cancel">Cancel</string>
<string name="nest_no_server_save_failed">Couldn\'t save your nest server list. Try again.</string>
<string name="nest_create_title">Start a new nest</string>
<string name="nest_create_field_room">Room name</string>
<string name="nest_create_field_summary">What\'s it about?</string>
<string name="nest_create_field_service">MoQ service URL</string>
<string name="nest_create_field_service_hint">Auth sidecar — defaults to nostrnests.com</string>
<string name="nest_create_field_endpoint">MoQ relay endpoint</string>
<string name="nest_create_field_endpoint_hint">WebTransport URL — usually the same host</string>
<string name="nest_create_field_image">Cover image URL (optional)</string>
<string name="nest_create_cancel">Cancel</string>
<string name="nest_create_submit">Start space</string>
<string name="nest_create_schedule_toggle">Schedule for later</string>
<string name="nest_create_when">Pick a start time</string>
<string name="nests_servers_title">Nest servers</string>
<string name="nests_servers_explainer">Choose which MoQ host servers Amethyst publishes your nests to. The first entry is used by default when you start a new space.</string>
<string name="nests_servers_my_section">Your servers</string>
<string name="nests_servers_my_explainer">Saved as a kind-10112 replaceable event so other clients can read your preference.</string>
<string name="nests_servers_add_field">Add an audio-room server</string>
<string name="nests_servers_add_field">Add a nest server</string>
<string name="nests_servers_recommended_section">Recommended servers</string>
<string name="nests_servers_recommended_explainer">Built-in suggestions you can add to your list with one tap.</string>
<string name="nests_servers_use_defaults">Use Amethyst defaults</string>
<string name="nests_servers_add_recommended">Add server</string>
<string name="nests_servers_remove">Remove server</string>
<string name="nests_servers_empty">No audio-room servers yet. Add one below or pick a recommended server.</string>
<string name="nests_servers_empty">No nest servers yet. Add one below or pick a recommended server.</string>
<string name="longs">Videos</string>
<string name="articles">Articles</string>
<string name="private_bookmarks">Private Bookmarks</string>
@@ -2141,7 +2141,7 @@
<string name="kind_profile_badges">Profile Badges</string>
<string name="kind_blocked_relays">Blocked Relays</string>
<string name="kind_blossom_servers">Blossom Servers</string>
<string name="kind_nests_servers">Audio-room (Nests) Servers</string>
<string name="kind_nests_servers">Nests Servers</string>
<string name="kind_blossom_auth">Blossom Auth</string>
<string name="kind_broadcast_relays">Broadcast Relays</string>
<string name="kind_bookmark_list">Bookmark List</string>
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag
@@ -27,7 +27,7 @@ import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class EditAudioRoomViewModelTest {
class EditNestViewModelTest {
private val host = "a".repeat(64)
private val speakerA = "b".repeat(64)
private val speakerB = "c".repeat(64)
@@ -68,7 +68,7 @@ class EditAudioRoomViewModelTest {
imageUrl: String = "",
endpointUrl: String = "https://moq.example.com",
serviceUrl: String = "https://auth.example.com",
) = EditAudioRoomViewModel.FormState(
) = EditNestViewModel.FormState(
dTag = dTag,
roomName = roomName,
summary = summary,
@@ -85,7 +85,7 @@ class EditAudioRoomViewModelTest {
val newForm = form(dTag = "rt-42", roomName = "New name", summary = "New summary")
val template =
EditAudioRoomViewModel.buildEditTemplate(src, newForm, StatusTag.STATUS.OPEN)
EditNestViewModel.buildEditTemplate(src, newForm, StatusTag.STATUS.OPEN)
// Same d-tag → same address → relay treats as replacement.
val dTag = template.tags.firstOrNull { it.firstOrNull() == "d" }?.getOrNull(1)
@@ -113,7 +113,7 @@ class EditAudioRoomViewModelTest {
val newForm = form(dTag = "rt-1", roomName = "Renamed")
val template =
EditAudioRoomViewModel.buildEditTemplate(src, newForm, StatusTag.STATUS.OPEN)
EditNestViewModel.buildEditTemplate(src, newForm, StatusTag.STATUS.OPEN)
val pTagPubkeys = template.tags.filter { it.firstOrNull() == "p" }.map { it[1] }
// Host plus two speakers — none can be lost on a rename.
@@ -129,7 +129,7 @@ class EditAudioRoomViewModelTest {
val sameForm = form(dTag = "rt-99", roomName = src.room().orEmpty())
val template =
EditAudioRoomViewModel.buildEditTemplate(src, sameForm, StatusTag.STATUS.CLOSED)
EditNestViewModel.buildEditTemplate(src, sameForm, StatusTag.STATUS.CLOSED)
val statusTag = template.tags.firstOrNull { it.firstOrNull() == "status" }?.getOrNull(1)
assertEquals(StatusTag.STATUS.CLOSED.code, statusTag)
@@ -143,7 +143,7 @@ class EditAudioRoomViewModelTest {
val emptied = form(dTag = "rt-1", roomName = "X", summary = " ", imageUrl = "")
val template =
EditAudioRoomViewModel.buildEditTemplate(src, emptied, StatusTag.STATUS.OPEN)
EditNestViewModel.buildEditTemplate(src, emptied, StatusTag.STATUS.OPEN)
// Blank inputs MUST NOT carry over from the original; otherwise
// a "delete the summary" edit would silently fail.
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE
@@ -33,7 +33,7 @@ import com.vitorpamplona.nestsclient.NestsSpeaker
import com.vitorpamplona.nestsclient.NestsSpeakerState
import com.vitorpamplona.nestsclient.audio.AudioCapture
import com.vitorpamplona.nestsclient.audio.AudioPlayer
import com.vitorpamplona.nestsclient.audio.AudioRoomPlayer
import com.vitorpamplona.nestsclient.audio.NestPlayer
import com.vitorpamplona.nestsclient.audio.OpusDecoder
import com.vitorpamplona.nestsclient.audio.OpusEncoder
import com.vitorpamplona.nestsclient.connectNestsSpeaker
@@ -60,7 +60,7 @@ import kotlin.coroutines.cancellation.CancellationException
* Per-screen state holder for a NIP-53 audio-room.
*
* Owns one [NestsListener] for the lifetime of the screen and one
* [AudioRoomPlayer] per active speaker subscription. The screen tells the
* [NestPlayer] per active speaker subscription. The screen tells the
* VM which speakers it cares about via [updateSpeakers]; subscribe / play
* happen automatically once the listener is [NestsListenerState.Connected].
*
@@ -90,7 +90,7 @@ import kotlin.coroutines.cancellation.CancellationException
* via `viewModelScope.launch(Dispatchers.Main.immediate) { ... }` first.
*/
@Stable
class AudioRoomViewModel(
class NestViewModel(
private val httpClient: NestsClient,
private val transport: WebTransportFactory,
private val decoderFactory: () -> OpusDecoder,
@@ -116,8 +116,8 @@ class AudioRoomViewModel(
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
private val cleanupScope: CoroutineScope = GlobalScope,
) : ViewModel() {
private val _uiState = MutableStateFlow(AudioRoomUiState())
val uiState: StateFlow<AudioRoomUiState> = _uiState.asStateFlow()
private val _uiState = MutableStateFlow(NestUiState())
val uiState: StateFlow<NestUiState> = _uiState.asStateFlow()
/**
* Listener-side aggregation of every peer's most recent kind-10312
@@ -292,7 +292,7 @@ class AudioRoomViewModel(
/**
* Toggle whether we hold a speaker slot. Tier-2's "leave the stage"
* tap flips this to `false`; the next kind-10312 heartbeat picks up
* the change via [AudioRoomUiState.onStageNow]. Does NOT stop a
* the change via [NestUiState.onStageNow]. Does NOT stop a
* running broadcast UI / caller is expected to call
* [BroadcastHandle.close] separately when leaving the stage.
*/
@@ -674,7 +674,7 @@ class AudioRoomViewModel(
/**
* Stop the per-speaker player + fire-and-forget UNSUBSCRIBE on the VM
* scope. Both `AudioRoomPlayer.stop()` and `SubscribeHandle.unsubscribe()`
* scope. Both `NestPlayer.stop()` and `SubscribeHandle.unsubscribe()`
* are suspend, so we route them through one coroutine instead of two.
*/
private fun closeSubscription(slot: ActiveSubscription) {
@@ -766,7 +766,7 @@ class AudioRoomViewModel(
}
try {
val isMuted = _uiState.value.isMuted
val roomPlayer = AudioRoomPlayer(decoder, player, viewModelScope)
val roomPlayer = NestPlayer(decoder, player, viewModelScope)
// Apply current mute state before play() opens the device so the
// first frame respects it.
player.setMutedSafe(isMuted)
@@ -834,7 +834,7 @@ class AudioRoomViewModel(
// listener.close() below tears down the MoQ session and drops
// every active subscription, so we don't need to call
// unsubscribe() per-handle here — but we DO need to await each
// AudioRoomPlayer.stop() so the native MediaCodec / AudioTrack
// NestPlayer.stop() so the native MediaCodec / AudioTrack
// is released after its decode loop has unwound.
val scope = if (finalCleanup) cleanupScope else viewModelScope
val players = activeSubscriptions.values.map { it.detach().first }
@@ -912,7 +912,7 @@ class AudioRoomViewModel(
val pubkey: String,
) {
private var handle: SubscribeHandle? = null
private var roomPlayer: AudioRoomPlayer? = null
private var roomPlayer: NestPlayer? = null
var player: AudioPlayer? = null
private set
var isPlaying: Boolean = false
@@ -920,7 +920,7 @@ class AudioRoomViewModel(
fun attach(
handle: SubscribeHandle,
roomPlayer: AudioRoomPlayer,
roomPlayer: NestPlayer,
player: AudioPlayer,
) {
this.handle = handle
@@ -931,12 +931,12 @@ class AudioRoomViewModel(
/**
* Hand the player + handle back to the caller's coroutine scope
* `AudioRoomPlayer.stop()` and `SubscribeHandle.unsubscribe()` are
* `NestPlayer.stop()` and `SubscribeHandle.unsubscribe()` are
* both suspend, and the caller has the right scope to await them
* (so native MediaCodec/AudioTrack release runs after the decode
* loop has unwound, per audit MoQ #11/#12).
*/
fun detach(): Pair<AudioRoomPlayer?, SubscribeHandle?> {
fun detach(): Pair<NestPlayer?, SubscribeHandle?> {
isPlaying = false
val p = roomPlayer
val h = handle
@@ -951,7 +951,7 @@ class AudioRoomViewModel(
}
}
// Platform-specific Factory lives in `amethyst/.../audiorooms/room/`,
// Platform-specific Factory lives in `amethyst/.../nests/room/`,
// not commonMain — the lifecycle KMP `ViewModelProvider.Factory`
// signature has been migrating between releases; a thin Android-side
// factory keeps that churn out of shared code.
@@ -1006,7 +1006,7 @@ private fun AudioPlayer.setMutedSafe(muted: Boolean) {
}
@Immutable
data class AudioRoomUiState(
data class NestUiState(
val connection: ConnectionUiState = ConnectionUiState.Idle,
val isMuted: Boolean = false,
/** Pubkeys we have an open MoQ subscription for. */
@@ -1025,11 +1025,11 @@ data class AudioRoomUiState(
* speaker subsequently goes quiet. Cleared on speaker removal.
*/
val connectingSpeakers: ImmutableSet<String> = persistentSetOf(),
/** Speaker / publisher state — only relevant when [AudioRoomViewModel.canBroadcast]. */
/** Speaker / publisher state — only relevant when [NestViewModel.canBroadcast]. */
val broadcast: BroadcastUiState = BroadcastUiState.Idle,
/**
* `true` when we hold a speaker slot vs being pure audience. Defaults
* to `true` for users with [AudioRoomViewModel.canBroadcast]; the
* to `true` for users with [NestViewModel.canBroadcast]; the
* "leave the stage" tap (Tier 2) flips it to `false`. Drives the
* `["onstage", "0|1"]` tag on emitted kind-10312 presence events.
*/
@@ -1076,7 +1076,7 @@ const val SPEAKING_TIMEOUT_MS: Long = 250L
/**
* How long a kind-7 reaction stays in
* [AudioRoomViewModel.recentReactions] before the eviction sweep
* [NestViewModel.recentReactions] before the eviction sweep
* drops it. Matches the duration of the floating-up animation in the
* SpeakerReactionOverlay.
*/
@@ -54,7 +54,7 @@ import kotlin.test.assertIs
import kotlin.test.assertTrue
/**
* Drives [AudioRoomViewModel] with a fake [NestsListenerConnector] and
* Drives [NestViewModel] with a fake [NestsListenerConnector] and
* verifies its state-flow transitions:
* - connect() shows Connecting before the underlying connector returns
* - listener emits Connected uiState collapses to Connected
@@ -62,13 +62,13 @@ import kotlin.test.assertTrue
* - setMuted updates uiState and propagates to active subscriptions
* - disconnect() returns to Idle and tears down the listener
*
* Subscribe-side (subscribeSpeaker AudioRoomPlayer.play) is exercised via
* Subscribe-side (subscribeSpeaker NestPlayer.play) is exercised via
* the existing nestsClient tests; the cross-module audio glue would need
* an internal SubscribeHandle constructor that's intentionally not visible
* here.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class AudioRoomViewModelTest {
class NestViewModelTest {
@BeforeTest
fun setupMainDispatcher() {
Dispatchers.setMain(UnconfinedTestDispatcher())
@@ -143,7 +143,7 @@ class AudioRoomViewModelTest {
// Defaults to true so a freshly-joined speaker advertises
// onstage=1 on the first heartbeat (the heartbeat in
// AudioRoomActivityContent reads this as the tag value).
// NestActivityContent reads this as the tag value).
assertTrue(vm.uiState.value.onStageNow)
vm.setOnStage(false)
assertFalse(vm.uiState.value.onStageNow)
@@ -332,20 +332,20 @@ class AudioRoomViewModelTest {
@Test
fun publishingNowDerivesFromBroadcastStateAndMute() {
// Idle / connecting / failed: never publishing.
assertFalse(AudioRoomUiState().publishingNow)
assertFalse(AudioRoomUiState(broadcast = BroadcastUiState.Connecting).publishingNow)
assertFalse(AudioRoomUiState(broadcast = BroadcastUiState.Failed("boom")).publishingNow)
assertFalse(NestUiState().publishingNow)
assertFalse(NestUiState(broadcast = BroadcastUiState.Connecting).publishingNow)
assertFalse(NestUiState(broadcast = BroadcastUiState.Failed("boom")).publishingNow)
// Broadcasting unmuted: publishing.
assertTrue(
AudioRoomUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = false)).publishingNow,
NestUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = false)).publishingNow,
)
// Broadcasting muted: NOT publishing — matches the wire-tag
// semantics ("publishing=1" means actually pushing packets,
// not "hold a slot but silenced").
assertFalse(
AudioRoomUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = true)).publishingNow,
NestUiState(broadcast = BroadcastUiState.Broadcasting(isMuted = true)).publishingNow,
)
}
@@ -425,8 +425,8 @@ class AudioRoomViewModelTest {
)
}
private fun TestScope.newViewModel(connect: suspend (CoroutineScope) -> NestsListener): AudioRoomViewModel =
AudioRoomViewModel(
private fun TestScope.newViewModel(connect: suspend (CoroutineScope) -> NestsListener): NestViewModel =
NestViewModel(
httpClient = NoopNestsClient,
transport = NoopWebTransportFactory,
decoderFactory = { NoopOpusDecoder },
@@ -452,7 +452,7 @@ class AudioRoomViewModelTest {
mutable.value = s
}
override suspend fun subscribeSpeaker(speakerPubkeyHex: String): SubscribeHandle = error("subscribeSpeaker not exercised in these tests — see AudioRoomPlayerTest in :nestsClient")
override suspend fun subscribeSpeaker(speakerPubkeyHex: String): SubscribeHandle = error("subscribeSpeaker not exercised in these tests — see NestPlayerTest in :nestsClient")
override suspend fun close() {
closeCallCount++
@@ -38,7 +38,7 @@ import java.util.concurrent.atomic.AtomicLong
* Moq-lite-backed [NestsListener]. Wraps a connected [MoqLiteSession]
* and exposes the same listener API the IETF [DefaultNestsListener]
* does, so [connectNestsListener] can swap the framing layer without
* changing the public surface that [com.vitorpamplona.amethyst.commons.viewmodels.AudioRoomViewModel]
* changing the public surface that [com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel]
* and downstream UI consume.
*
* Subscription mapping per the audio-rooms NIP draft + nests JS
@@ -21,7 +21,7 @@
package com.vitorpamplona.nestsclient
import com.vitorpamplona.nestsclient.audio.AudioCapture
import com.vitorpamplona.nestsclient.audio.AudioRoomMoqLiteBroadcaster
import com.vitorpamplona.nestsclient.audio.NestMoqLiteBroadcaster
import com.vitorpamplona.nestsclient.audio.OpusEncoder
import com.vitorpamplona.nestsclient.moq.lite.MoqLitePublisherHandle
import com.vitorpamplona.nestsclient.moq.lite.MoqLiteSession
@@ -76,7 +76,7 @@ class MoqLiteNestsSpeaker internal constructor(
throw t
}
val broadcaster =
AudioRoomMoqLiteBroadcaster(
NestMoqLiteBroadcaster(
capture = captureFactory(),
encoder = encoderFactory(),
publisher = publisher,
@@ -139,7 +139,7 @@ class MoqLiteNestsSpeaker internal constructor(
}
internal class MoqLiteBroadcastHandle(
private val broadcaster: AudioRoomMoqLiteBroadcaster,
private val broadcaster: NestMoqLiteBroadcaster,
private val publisher: MoqLitePublisherHandle,
private val parent: MoqLiteNestsSpeaker,
) : BroadcastHandle {
@@ -21,7 +21,7 @@
package com.vitorpamplona.nestsclient
import com.vitorpamplona.nestsclient.audio.AudioCapture
import com.vitorpamplona.nestsclient.audio.AudioRoomBroadcaster
import com.vitorpamplona.nestsclient.audio.NestBroadcaster
import com.vitorpamplona.nestsclient.audio.OpusEncoder
import com.vitorpamplona.nestsclient.moq.MoqProtocolException
import com.vitorpamplona.nestsclient.moq.MoqSession
@@ -133,7 +133,7 @@ sealed class NestsSpeakerState {
* unit-test suite (`NestsSpeakerTest`) and for any future IETF
* MoQ-transport target.
*
* Wraps a connected [MoqSession] and plumbs an [AudioRoomBroadcaster]
* Wraps a connected [MoqSession] and plumbs an [NestBroadcaster]
* through it on [startBroadcasting]. Construction does NOT open the
* transport.
*/
@@ -168,7 +168,7 @@ class DefaultNestsSpeaker internal constructor(
throw t
}
val broadcaster =
AudioRoomBroadcaster(
NestBroadcaster(
capture = captureFactory(),
encoder = encoderFactory(),
publisher = publisher,
@@ -236,7 +236,7 @@ class DefaultNestsSpeaker internal constructor(
}
internal class DefaultBroadcastHandle(
private val broadcaster: AudioRoomBroadcaster,
private val broadcaster: NestBroadcaster,
private val announce: MoqSession.AnnounceHandle,
private val parent: DefaultNestsSpeaker,
) : BroadcastHandle {
@@ -28,7 +28,7 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
/**
* Inverse of [AudioRoomPlayer]: pulls PCM from an [AudioCapture], runs it
* Inverse of [NestPlayer]: pulls PCM from an [AudioCapture], runs it
* through an [OpusEncoder], and pushes the resulting Opus packets into a
* [MoqSession.TrackPublisher] as MoQ OBJECT_DATAGRAMs.
*
@@ -46,7 +46,7 @@ import kotlinx.coroutines.launch
* Encode failures are reported via [onError] but do NOT tear the loop down
* one bad frame shouldn't end the broadcast.
*/
class AudioRoomBroadcaster(
class NestBroadcaster(
private val capture: AudioCapture,
private val encoder: OpusEncoder,
private val publisher: MoqSession.TrackPublisher,
@@ -66,8 +66,8 @@ class AudioRoomBroadcaster(
* surface it to the user.
*/
fun start(onError: (AudioException) -> Unit = { /* swallow */ }) {
check(!stopped) { "AudioRoomBroadcaster already stopped" }
check(job == null) { "AudioRoomBroadcaster.start already called" }
check(!stopped) { "NestBroadcaster already stopped" }
check(job == null) { "NestBroadcaster.start already called" }
// Audit round-2 MoQ #7: capture.start() can throw before we have
// a job. Mark stopped + propagate so a half-started capture isn't
@@ -28,7 +28,7 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
/**
* Mirror of [AudioRoomBroadcaster] but driving a moq-lite
* Mirror of [NestBroadcaster] but driving a moq-lite
* [MoqLitePublisherHandle] instead of an IETF `MoqSession.TrackPublisher`.
* Keeps the IETF broadcaster intact for the IETF unit-test suite while
* letting the production speaker path use moq-lite.
@@ -36,7 +36,7 @@ import kotlinx.coroutines.launch
* Lifecycle and audit comments mirror the IETF version 1:1 only the
* sink type changes.
*/
class AudioRoomMoqLiteBroadcaster(
class NestMoqLiteBroadcaster(
private val capture: AudioCapture,
private val encoder: OpusEncoder,
private val publisher: MoqLitePublisherHandle,
@@ -55,8 +55,8 @@ class AudioRoomMoqLiteBroadcaster(
* the exception propagates.
*/
fun start(onError: (AudioException) -> Unit = { /* swallow */ }) {
check(!stopped) { "AudioRoomMoqLiteBroadcaster already stopped" }
check(job == null) { "AudioRoomMoqLiteBroadcaster.start already called" }
check(!stopped) { "NestMoqLiteBroadcaster already stopped" }
check(job == null) { "NestMoqLiteBroadcaster.start already called" }
try {
capture.start()
@@ -33,7 +33,7 @@ import kotlinx.coroutines.launch
* through an [OpusDecoder] into an [AudioPlayer].
*
* Single-track. To play multiple speakers in a room, instantiate one
* [AudioRoomPlayer] per [com.vitorpamplona.nestsclient.moq.SubscribeHandle];
* [NestPlayer] per [com.vitorpamplona.nestsclient.moq.SubscribeHandle];
* each owns its own decoder (Opus state is per-track).
*
* Lifecycle:
@@ -41,7 +41,7 @@ import kotlinx.coroutines.launch
* - [stop] cancels the decode loop, stops the player, and releases the
* decoder. Idempotent.
*/
class AudioRoomPlayer(
class NestPlayer(
private val decoder: OpusDecoder,
private val player: AudioPlayer,
private val scope: CoroutineScope,
@@ -61,8 +61,8 @@ class AudioRoomPlayer(
objects: Flow<MoqObject>,
onError: (AudioException) -> Unit = { /* swallow */ },
) {
check(!stopped) { "AudioRoomPlayer already stopped" }
check(job == null) { "AudioRoomPlayer.play already called" }
check(!stopped) { "NestPlayer already stopped" }
check(job == null) { "NestPlayer.play already called" }
player.start()
job =
@@ -28,14 +28,14 @@ import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class AudioRoomBroadcasterTest {
class NestBroadcasterTest {
@Test
fun pcm_frames_flow_through_encoder_into_publisher_in_order() =
runTest {
val capture = ScriptedCapture(listOf(shortArrayOf(1), shortArrayOf(2), shortArrayOf(3)))
val encoder = ScriptedEncoder(prefix = byteArrayOf(0xAA.toByte()))
val publisher = RecordingPublisher("track")
val broadcaster = AudioRoomBroadcaster(capture, encoder, publisher, backgroundScope)
val broadcaster = NestBroadcaster(capture, encoder, publisher, backgroundScope)
broadcaster.start()
// Capture exhausts after 3 frames (readFrame returns null) so the
@@ -61,7 +61,7 @@ class AudioRoomBroadcasterTest {
val capture = ScriptedCapture(listOf(shortArrayOf(1), shortArrayOf(2), shortArrayOf(3)))
val encoder = ScriptedEncoder(prefix = byteArrayOf(0xBB.toByte()))
val publisher = RecordingPublisher("track")
val broadcaster = AudioRoomBroadcaster(capture, encoder, publisher, backgroundScope)
val broadcaster = NestBroadcaster(capture, encoder, publisher, backgroundScope)
broadcaster.setMuted(true)
broadcaster.start()
@@ -81,7 +81,7 @@ class AudioRoomBroadcasterTest {
val capture = ScriptedCapture(listOf(shortArrayOf(1), shortArrayOf(2)))
val encoder = ScriptedEncoder(prefix = byteArrayOf(0xCC.toByte()), warmupSkips = 1)
val publisher = RecordingPublisher("track")
val broadcaster = AudioRoomBroadcaster(capture, encoder, publisher, backgroundScope)
val broadcaster = NestBroadcaster(capture, encoder, publisher, backgroundScope)
broadcaster.start()
capture.awaitDrained()
@@ -100,7 +100,7 @@ class AudioRoomBroadcasterTest {
val capture = ScriptedCapture(listOf(shortArrayOf(1), shortArrayOf(2), shortArrayOf(3)))
val encoder = ScriptedEncoder(prefix = byteArrayOf(0xDD.toByte()), failOnNthCall = 1)
val publisher = RecordingPublisher("track")
val broadcaster = AudioRoomBroadcaster(capture, encoder, publisher, backgroundScope)
val broadcaster = NestBroadcaster(capture, encoder, publisher, backgroundScope)
val errors = mutableListOf<AudioException>()
broadcaster.start { errors.add(it) }
@@ -117,7 +117,7 @@ class AudioRoomBroadcasterTest {
val capture = ScriptedCapture(listOf(shortArrayOf(1)))
val encoder = ScriptedEncoder(prefix = byteArrayOf(0xEE.toByte()))
val publisher = RecordingPublisher("track")
val broadcaster = AudioRoomBroadcaster(capture, encoder, publisher, backgroundScope)
val broadcaster = NestBroadcaster(capture, encoder, publisher, backgroundScope)
broadcaster.start()
capture.awaitDrained()
@@ -31,7 +31,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class AudioRoomPlayerTest {
class NestPlayerTest {
@Test
fun every_object_payload_is_decoded_and_enqueued_in_order() =
runTest {
@@ -45,7 +45,7 @@ class AudioRoomPlayerTest {
moqObject(byteArrayOf(0x04, 0x05, 0x06)),
)
val sut = AudioRoomPlayer(decoder, player, this)
val sut = NestPlayer(decoder, player, this)
sut.play(objects)
testScheduler.advanceUntilIdle()
@@ -83,7 +83,7 @@ class AudioRoomPlayerTest {
moqObject(byteArrayOf(0x02)),
)
val sut = AudioRoomPlayer(decoder, player, this)
val sut = NestPlayer(decoder, player, this)
sut.play(objects, onError = { errors.add(it) })
testScheduler.advanceUntilIdle()
@@ -102,7 +102,7 @@ class AudioRoomPlayerTest {
runTest {
val decoder = FakeOpusDecoder { byteToShorts(it) }
val player = FakeAudioPlayer()
val sut = AudioRoomPlayer(decoder, player, this)
val sut = NestPlayer(decoder, player, this)
sut.play(flowOf())
testScheduler.advanceUntilIdle()
sut.stop()
@@ -115,7 +115,7 @@ class AudioRoomPlayerTest {
fun play_cannot_be_called_twice_on_the_same_instance() =
runTest {
val sut =
AudioRoomPlayer(
NestPlayer(
FakeOpusDecoder { byteToShorts(it) },
FakeAudioPlayer(),
this,
@@ -129,7 +129,7 @@ class AudioRoomPlayerTest {
fun play_after_stop_is_rejected() =
runTest {
val sut =
AudioRoomPlayer(
NestPlayer(
FakeOpusDecoder { byteToShorts(it) },
FakeAudioPlayer(),
this,
@@ -143,7 +143,7 @@ class AudioRoomPlayerTest {
runTest {
val decoder = FakeOpusDecoder { ShortArray(0) }
val player = FakeAudioPlayer()
val sut = AudioRoomPlayer(decoder, player, this)
val sut = NestPlayer(decoder, player, this)
sut.play(flowOf(moqObject(byteArrayOf(0x01))))
testScheduler.advanceUntilIdle()
assertEquals(0, player.queued.size)
@@ -157,7 +157,7 @@ class AudioRoomPlayerTest {
val decoder = FakeOpusDecoder { byteToShorts(it) }
val player = FakeAudioPlayer()
val sut = AudioRoomPlayer(decoder, player, this)
val sut = NestPlayer(decoder, player, this)
sut.play(channel.receiveAsFlow())
testScheduler.runCurrent()
@@ -51,7 +51,7 @@ import kotlin.test.fail
* - the broadcaster keeps the capture pump running while muted (so
* unmute is sample-accurate)
* - frames pushed during mute do NOT reach subscribers (`if (muted)
* continue` in [com.vitorpamplona.nestsclient.audio.AudioRoomMoqLiteBroadcaster])
* continue` in [com.vitorpamplona.nestsclient.audio.NestMoqLiteBroadcaster])
* - frames pushed before / after the muted window DO reach
* subscribers, with the muted ones missing entirely (no silent
* placeholder frames)
@@ -23,7 +23,7 @@ package com.vitorpamplona.nestsclient.interop
import com.vitorpamplona.nestsclient.NestsRoomConfig
import com.vitorpamplona.nestsclient.OkHttpNestsClient
import com.vitorpamplona.nestsclient.audio.AudioCapture
import com.vitorpamplona.nestsclient.audio.AudioRoomMoqLiteBroadcaster
import com.vitorpamplona.nestsclient.audio.NestMoqLiteBroadcaster
import com.vitorpamplona.nestsclient.audio.OpusEncoder
import com.vitorpamplona.nestsclient.connectNestsListener
import com.vitorpamplona.nestsclient.connectNestsSpeaker
@@ -57,7 +57,7 @@ import kotlin.test.fail
* (`MoqLitePublisherHandle`), the listener subscribes by speaker
* pubkey (`broadcast=pubkey`, `track="audio/data"`), the speaker
* pushes deterministic Opus-shaped payloads through the
* [AudioRoomMoqLiteBroadcaster] pipeline, and the listener collects
* [NestMoqLiteBroadcaster] pipeline, and the listener collects
* them via [SubscribeHandle.objects] (which the moq-lite adapter
* wraps over `MoqLiteFrame` so existing decoder / player code keeps
* working).
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.experimental.audiorooms.admin
package com.vitorpamplona.quartz.experimental.nests.admin
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -50,7 +50,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
* }
*
* The `server` URL points at the moq-rs / moq-auth deployment's base
* URL Amethyst's `CreateAudioRoomSheet` uses it for both the
* URL Amethyst's `CreateNestSheet` uses it for both the
* `service` (auth sidecar) and `endpoint` (WebTransport relay) tags
* on the kind-30312 event, since nostrnests's reference deployment
* co-locates them. A future revision can split the two into separate
@@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.audiorooms.admin.AdminCommandEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
@@ -36,6 +35,7 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.experimental.nests.admin.AdminCommandEvent
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.experimental.audiorooms.admin
package com.vitorpamplona.quartz.experimental.nests.admin
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import kotlin.test.Test