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