Skip to content

Commit db713d9

Browse files
committed
Determine target screen in Party list screen in advance
1 parent bb8cf07 commit db713d9

File tree

10 files changed

+109
-34
lines changed

10 files changed

+109
-34
lines changed

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/DependencyInjection.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ val appModule =
349349
)
350350
}
351351
bindProvider { InvitationScreenModel(instance(), instance(), instance()) }
352-
bindProvider { PartyListScreenModel(instance()) }
352+
bindProvider { PartyListScreenModel(instance(), instance()) }
353353
bindProvider { SettingsScreenModel(instance(), instance()) }
354354
bindFactory { partyId: PartyId ->
355355
CharacterCreationScreenModel(

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/character/CharacterRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cz.frantisekmasa.wfrp_master.common.core.domain.character
22

33
import arrow.core.Either
44
import com.benasher44.uuid.Uuid
5+
import cz.frantisekmasa.wfrp_master.common.core.auth.UserId
56
import cz.frantisekmasa.wfrp_master.common.core.domain.identifiers.CharacterId
67
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
78
import dev.gitlive.firebase.firestore.Transaction
@@ -31,6 +32,8 @@ interface CharacterRepository {
3132
partyId: PartyId,
3233
): Boolean
3334

35+
fun getPlayerCharactersInAllPartiesLive(userId: UserId): Flow<List<Pair<PartyId, Character>>>
36+
3437
suspend fun findByCompendiumCareer(
3538
partyId: PartyId,
3639
careerId: Uuid,

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/firebase/repositories/FirestoreCharacterRepository.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cz.frantisekmasa.wfrp_master.common.core.firebase.repositories
33
import arrow.core.left
44
import arrow.core.right
55
import com.benasher44.uuid.Uuid
6+
import com.benasher44.uuid.uuidFrom
7+
import cz.frantisekmasa.wfrp_master.common.core.auth.UserId
68
import cz.frantisekmasa.wfrp_master.common.core.domain.character.Character
79
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterNotFound
810
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterRepository
@@ -21,7 +23,7 @@ import kotlinx.coroutines.flow.Flow
2123
import kotlinx.coroutines.flow.map
2224

2325
class FirestoreCharacterRepository(
24-
firestore: FirebaseFirestore,
26+
private val firestore: FirebaseFirestore,
2527
) : CharacterRepository {
2628
private val parties = firestore.collection(Schema.PARTIES)
2729

@@ -92,6 +94,18 @@ class FirestoreCharacterRepository(
9294
.isNotEmpty()
9395
}
9496

97+
override fun getPlayerCharactersInAllPartiesLive(userId: UserId): Flow<List<Pair<PartyId, Character>>> {
98+
return firestore.collectionGroup(Schema.CHARACTERS)
99+
.where { ("userId" equalTo userId.toString()) and ("archived" equalTo false) }
100+
.snapshots
101+
.map { snapshot ->
102+
snapshot.documents.map {
103+
PartyId(uuidFrom(it.reference.parent.parent!!.id)) to
104+
it.data(Character.serializer())
105+
}
106+
}
107+
}
108+
95109
override suspend fun findByCompendiumCareer(
96110
partyId: PartyId,
97111
careerId: Uuid,

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/partyList/LeavePartyDialog.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import androidx.compose.runtime.rememberCoroutineScope
1212
import androidx.compose.runtime.setValue
1313
import cz.frantisekmasa.wfrp_master.common.Str
1414
import cz.frantisekmasa.wfrp_master.common.core.auth.LocalUser
15-
import cz.frantisekmasa.wfrp_master.common.core.domain.party.Party
1615
import cz.frantisekmasa.wfrp_master.common.core.ui.dialogs.AlertDialog
1716
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.Spacing
1817
import dev.icerock.moko.resources.compose.stringResource
@@ -21,7 +20,7 @@ import kotlinx.coroutines.launch
2120

2221
@Composable
2322
fun LeavePartyDialog(
24-
party: Party,
23+
party: PartyListItem,
2524
screenModel: PartyListScreenModel,
2625
onDismissRequest: () -> Unit,
2726
) {

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/partyList/PartyListScreen.kt

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ import cz.frantisekmasa.wfrp_master.common.Plurals
3131
import cz.frantisekmasa.wfrp_master.common.Str
3232
import cz.frantisekmasa.wfrp_master.common.changelog.ChangelogAction
3333
import cz.frantisekmasa.wfrp_master.common.changelog.ChangelogScreen
34+
import cz.frantisekmasa.wfrp_master.common.character.CharacterDetailScreen
3435
import cz.frantisekmasa.wfrp_master.common.character.CharacterPickerScreen
3536
import cz.frantisekmasa.wfrp_master.common.core.LocalStaticConfiguration
3637
import cz.frantisekmasa.wfrp_master.common.core.auth.LocalUser
3738
import cz.frantisekmasa.wfrp_master.common.core.config.Platform
39+
import cz.frantisekmasa.wfrp_master.common.core.domain.identifiers.CharacterId
3840
import cz.frantisekmasa.wfrp_master.common.core.domain.party.Party
41+
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
3942
import cz.frantisekmasa.wfrp_master.common.core.shared.Resources
4043
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.HamburgerButton
4144
import cz.frantisekmasa.wfrp_master.common.core.ui.dialogs.DialogState
@@ -52,6 +55,8 @@ import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.VISUAL_ONLY_ICON_D
5255
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.rememberScreenModel
5356
import cz.frantisekmasa.wfrp_master.common.gameMaster.GameMasterScreen
5457
import cz.frantisekmasa.wfrp_master.common.invitation.InvitationScannerScreen
58+
import dev.icerock.moko.parcelize.Parcelable
59+
import dev.icerock.moko.parcelize.Parcelize
5560
import dev.icerock.moko.resources.compose.stringResource
5661

5762
object PartyListScreen : Screen {
@@ -108,11 +113,11 @@ object PartyListScreen : Screen {
108113
},
109114
) {
110115
var removePartyDialogState by remember {
111-
mutableStateOf<DialogState<Party>>(DialogState.Closed())
116+
mutableStateOf<DialogState<PartyListItem>>(DialogState.Closed())
112117
}
113118

114119
var leavePartyDialogState by remember {
115-
mutableStateOf<DialogState<Party>>(DialogState.Closed())
120+
mutableStateOf<DialogState<PartyListItem>>(DialogState.Closed())
116121
}
117122

118123
removePartyDialogState.IfOpened { party ->
@@ -138,10 +143,10 @@ object PartyListScreen : Screen {
138143
parties,
139144
onClick = {
140145
navigation.navigate(
141-
if (it.gameMasterId == userId) {
142-
GameMasterScreen(it.id)
143-
} else {
144-
CharacterPickerScreen(it.id)
146+
when {
147+
it.isGameMaster -> GameMasterScreen(it.id)
148+
it.singleCharacterId != null -> CharacterDetailScreen(it.singleCharacterId)
149+
else -> CharacterPickerScreen(it.id)
145150
},
146151
)
147152
},
@@ -191,13 +196,13 @@ private fun Menu(
191196
}
192197

193198
@Composable
194-
fun PartyItem(party: Party) {
199+
fun PartyItem(item: PartyListItem) {
195200
Column {
196-
val playersCount = party.playersCount
201+
val playersCount = item.playersCount
197202

198203
ListItem(
199204
icon = { ItemIcon(Icons.Rounded.Group, ItemIcon.Size.Large) },
200-
text = { Text(party.name) },
205+
text = { Text(item.name) },
201206
trailing =
202207
if (playersCount > 0) {
203208
({ Text(stringResource(Plurals.parties_player_count, playersCount, playersCount)) })
@@ -212,10 +217,10 @@ fun PartyItem(party: Party) {
212217
@Composable
213218
private fun MainContainer(
214219
modifier: Modifier,
215-
parties: List<Party>?,
216-
onClick: (Party) -> Unit,
217-
onRemove: (Party) -> Unit,
218-
onLeaveRequest: (Party) -> Unit,
220+
parties: List<PartyListItem>?,
221+
onClick: (PartyListItem) -> Unit,
222+
onRemove: (PartyListItem) -> Unit,
223+
onLeaveRequest: (PartyListItem) -> Unit,
219224
) {
220225
Box(modifier) {
221226
parties?.let {
@@ -240,38 +245,45 @@ private fun MainContainer(
240245

241246
@Composable
242247
fun PartyList(
243-
parties: List<Party>,
244-
onClick: (Party) -> Unit,
245-
onRemove: (Party) -> Unit,
246-
onLeaveRequest: (Party) -> Unit,
248+
parties: List<PartyListItem>,
249+
onClick: (PartyListItem) -> Unit,
250+
onRemove: (PartyListItem) -> Unit,
251+
onLeaveRequest: (PartyListItem) -> Unit,
247252
) {
248253
LazyColumn(
249254
Modifier.fillMaxHeight(),
250255
contentPadding = PaddingValues(top = Spacing.medium, bottom = Spacing.bottomPaddingUnderFab),
251256
) {
252-
items(parties, key = { it.id }) { party ->
253-
val isGameMaster =
254-
LocalUser.current.id == party.gameMasterId || party.gameMasterId == null
255-
257+
items(parties, key = { it.id }) { item ->
256258
WithContextMenu(
257-
onClick = { onClick(party) },
259+
onClick = { onClick(item) },
258260
items =
259261
listOf(
260-
if (isGameMaster) {
262+
if (item.isGameMaster) {
261263
ContextMenu.Item(
262264
stringResource(Str.common_ui_button_remove),
263-
onClick = { onRemove(party) },
265+
onClick = { onRemove(item) },
264266
)
265267
} else {
266268
ContextMenu.Item(
267269
stringResource(Str.parties_button_leave),
268-
onClick = { onLeaveRequest(party) },
270+
onClick = { onLeaveRequest(item) },
269271
)
270272
},
271273
),
272274
) {
273-
PartyItem(party)
275+
PartyItem(item)
274276
}
275277
}
276278
}
277279
}
280+
281+
@Parcelize
282+
data class PartyListItem(
283+
val id: PartyId,
284+
val party: Party,
285+
val name: String,
286+
val isGameMaster: Boolean,
287+
val singleCharacterId: CharacterId?,
288+
val playersCount: Int,
289+
) : Parcelable

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/partyList/PartyListScreenModel.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cz.frantisekmasa.wfrp_master.common.partyList
22

33
import cafe.adriel.voyager.core.model.ScreenModel
44
import cz.frantisekmasa.wfrp_master.common.core.auth.UserId
5+
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterRepository
6+
import cz.frantisekmasa.wfrp_master.common.core.domain.identifiers.CharacterId
57
import cz.frantisekmasa.wfrp_master.common.core.domain.party.Party
68
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
79
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyRepository
@@ -10,12 +12,30 @@ import cz.frantisekmasa.wfrp_master.common.core.logging.Reporter
1012
import cz.frantisekmasa.wfrp_master.common.settings.Language
1113
import io.github.aakira.napier.Napier
1214
import kotlinx.coroutines.flow.Flow
15+
import kotlinx.coroutines.flow.combine
1316

1417
class PartyListScreenModel(
1518
private val parties: PartyRepository,
19+
private val characters: CharacterRepository,
1620
) : ScreenModel {
17-
fun liveForUser(userId: UserId): Flow<List<Party>> {
18-
return parties.forUserLive(userId)
21+
fun liveForUser(userId: UserId): Flow<List<PartyListItem>> {
22+
return combine(
23+
parties.forUserLive(userId),
24+
characters.getPlayerCharactersInAllPartiesLive(userId),
25+
) { parties, characters ->
26+
val charactersByParty = characters.groupBy({ it.first }) { CharacterId(it.first, it.second.id) }
27+
28+
parties.map { party ->
29+
PartyListItem(
30+
id = party.id,
31+
party = party,
32+
name = party.name,
33+
isGameMaster = party.gameMasterId == userId || party.gameMasterId == null,
34+
singleCharacterId = charactersByParty[party.id]?.singleOrNull(),
35+
playersCount = party.playersCount,
36+
)
37+
}
38+
}
1939
}
2040

2141
suspend fun archive(partyId: PartyId) {

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/partyList/RemovePartyDialog.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import androidx.compose.runtime.rememberCoroutineScope
1313
import androidx.compose.runtime.setValue
1414
import androidx.compose.ui.text.font.FontWeight
1515
import cz.frantisekmasa.wfrp_master.common.Str
16-
import cz.frantisekmasa.wfrp_master.common.core.domain.party.Party
1716
import cz.frantisekmasa.wfrp_master.common.core.ui.dialogs.AlertDialog
1817
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.Spacing
1918
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder
@@ -23,7 +22,7 @@ import kotlinx.coroutines.launch
2322

2423
@Composable
2524
fun RemovePartyDialog(
26-
party: Party,
25+
party: PartyListItem,
2726
screenModel: PartyListScreenModel,
2827
onDismissRequest: () -> Unit,
2928
) {

common/src/commonTest/kotlin/cz/frantisekmasa/wfrp_master/common/dummies/DummyCharacterRepository.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cz.frantisekmasa.wfrp_master.common.dummies
33
import arrow.core.Either
44
import arrow.core.rightIfNotNull
55
import com.benasher44.uuid.Uuid
6+
import cz.frantisekmasa.wfrp_master.common.core.auth.UserId
67
import cz.frantisekmasa.wfrp_master.common.core.domain.character.Character
78
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterNotFound
89
import cz.frantisekmasa.wfrp_master.common.core.domain.character.CharacterRepository
@@ -50,6 +51,21 @@ class DummyCharacterRepository : CharacterRepository {
5051
return characters[partyId]?.any { it.value.userId?.toString() == userId } ?: false
5152
}
5253

54+
override fun getPlayerCharactersInAllPartiesLive(userId: UserId): Flow<List<Pair<PartyId, Character>>> {
55+
return flowOf(
56+
characters.entries
57+
.asSequence()
58+
.flatMap { (partyId, characters) ->
59+
characters
60+
.values
61+
.asSequence()
62+
.filter { it.userId == userId }
63+
.map { partyId to it }
64+
}
65+
.toList(),
66+
)
67+
}
68+
5369
override suspend fun findByCompendiumCareer(
5470
partyId: PartyId,
5571
careerId: Uuid,

firebase/firestore.indexes.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
{ "fieldPath": "type", "order": "ASCENDING" },
99
{ "fieldPath": "name", "order": "ASCENDING" }
1010
]
11+
},
12+
{
13+
"collectionGroup": "characters",
14+
"queryScope": "COLLECTION_GROUP",
15+
"fields": [
16+
{ "fieldPath": "userId", "order": "ASCENDING" },
17+
{ "fieldPath": "archived", "order": "ASCENDING" }
18+
]
1119
}
1220
],
1321
"fieldOverrides": []

firebase/firestore.rules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ rules_version = '2';
22

33
service cloud.firestore {
44
match /databases/{database}/documents {
5+
// Let users search for all their Characters across the parties
6+
match /{path=**}/characters/{characterId} {
7+
allow read: if request.auth.uid != null || resource.data.userId == request.auth.uid;
8+
}
59

610
match /parties/{party=**} {
711
allow read: if request.auth.uid != null

0 commit comments

Comments
 (0)