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