Коммит 51959edd создал по автору Radch-enko's avatar Radch-enko
Просмотр файлов

Refactor: remove `EventManagerRepository` and associated logic

- Deleted `DefaultEventRepositoryMediator`, `EventManagerRepositoryImpl`, and related interfaces (`EventManagerRepository`, `EventRepositoryMediator`) for improved modularity.
- Replaced redundant repository implementations with `LocalBookingRepositoryImpl` and `BookingRepositoryImpl`.
- Introduced specialized use cases (`GetRoomsInfoUseCase`, `GetCurrentRoomInfosUseCase`) to simplify domain logic and reduce coupling.
- Updated existing components (`FreeUpRoomUseCase`, etc.) to utilize the new repository and use case structure.
- Removed outdated Xcode scheme configurations to streamline the project build settings.
владелец 099cd6f7
......@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.update
* @param T Type of data to collect
* @param defaultValue Default value for the data
*/
// TODO rename
class Collector<T>(defaultValue: T) {
private data class CollectableElement<T>(val value: T, val number: Long)
......
......@@ -7,17 +7,20 @@ import band.effective.office.tablet.core.data.api.WorkspaceApi
import band.effective.office.tablet.core.data.api.impl.BookingApiImpl
import band.effective.office.tablet.core.data.api.impl.UserApiImpl
import band.effective.office.tablet.core.data.api.impl.WorkspaceApiImpl
import band.effective.office.tablet.core.data.mapper.EventInfoMapper
import band.effective.office.tablet.core.data.mapper.RoomInfoMapper
import band.effective.office.tablet.core.data.network.HttpClientProvider
import band.effective.office.tablet.core.data.repository.DefaultEventRepositoryMediator
import band.effective.office.tablet.core.data.repository.EventManagerRepositoryImpl
import band.effective.office.tablet.core.data.repository.LocalEventStoreRepository
import band.effective.office.tablet.core.data.repository.NetworkEventRepository
import band.effective.office.tablet.core.data.repository.BookingRepositoryImpl
import band.effective.office.tablet.core.data.repository.LocalBookingRepositoryImpl
import band.effective.office.tablet.core.data.repository.LocalRoomRepositoryImpl
import band.effective.office.tablet.core.data.repository.OrganizerRepositoryImpl
import band.effective.office.tablet.core.data.repository.RoomRepositoryImpl
import band.effective.office.tablet.core.data.utils.Buffer
import band.effective.office.tablet.core.domain.repository.BookingRepository
import band.effective.office.tablet.core.domain.repository.EventManagerRepository
import band.effective.office.tablet.core.domain.repository.EventRepositoryMediator
import band.effective.office.tablet.core.domain.repository.LocalBookingRepository
import band.effective.office.tablet.core.domain.repository.LocalRoomRepository
import band.effective.office.tablet.core.domain.repository.OrganizerRepository
import band.effective.office.tablet.core.domain.repository.RoomRepository
import org.koin.dsl.module
/**
......@@ -30,6 +33,10 @@ val dataModule = module {
// Collectors
single { Collector("") }
// Mappers
single { EventInfoMapper() }
single { RoomInfoMapper(get()) }
// API implementations
single<BookingApi> { BookingApiImpl(get()) }
......@@ -37,32 +44,32 @@ val dataModule = module {
single<WorkspaceApi> { WorkspaceApiImpl(get()) }
// Repository implementations
single<OrganizerRepository> {
OrganizerRepositoryImpl(api = get())
}
single<BookingRepository> {
NetworkEventRepository(api = get(), workspaceApi = get())
}
single<LocalBookingRepository> {
LocalEventStoreRepository()
LocalBookingRepositoryImpl(buffer = get())
}
// Mediator for coordinating between repositories
single<EventRepositoryMediator> {
DefaultEventRepositoryMediator(
networkRepository = get(),
localRepository = get()
single<BookingRepository> {
BookingRepositoryImpl(
api = get(),
eventInfoMapper = get(),
)
}
single<EventManagerRepository> {
EventManagerRepositoryImpl(
networkEventRepository = get(),
localEventStoreRepository = get(),
mediator = get()
single<LocalRoomRepository> {
LocalRoomRepositoryImpl(buffer = get())
}
single<RoomRepository> {
RoomRepositoryImpl(
api = get(),
workspaceApi = get(),
roomInfoMapper = get(),
)
}
single { Buffer() }
}
package band.effective.office.tablet.core.data.mapper
import band.effective.office.tablet.core.data.dto.booking.BookingRequestDTO
import band.effective.office.tablet.core.data.dto.booking.BookingResponseDTO
import band.effective.office.tablet.core.data.utils.Converter.toOrganizer
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.Organizer
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.util.asInstant
import band.effective.office.tablet.core.domain.util.asLocalDateTime
import kotlinx.datetime.Instant
class EventInfoMapper {
fun map(dto: BookingResponseDTO): EventInfo =
EventInfo(
id = dto.id,
startTime = Instant.fromEpochMilliseconds(dto.beginBooking).asLocalDateTime,
finishTime = Instant.fromEpochMilliseconds(dto.endBooking).asLocalDateTime,
organizer = dto.owner?.toOrganizer() ?: Organizer.default,
isLoading = false,
)
fun mapToRequest(eventInfo: EventInfo, roomInfo: RoomInfo): BookingRequestDTO = BookingRequestDTO(
beginBooking = eventInfo.startTime.asInstant.toEpochMilliseconds(),
endBooking = eventInfo.finishTime.asInstant.toEpochMilliseconds(),
ownerEmail = eventInfo.organizer.email,
participantEmails = listOfNotNull(eventInfo.organizer.email),
workspaceId = roomInfo.id
)
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.data.mapper
import band.effective.office.tablet.core.data.dto.workspace.WorkspaceDTO
import band.effective.office.tablet.core.domain.model.RoomInfo
class RoomInfoMapper(
private val eventInfoMapper: EventInfoMapper,
) {
fun map(dto: WorkspaceDTO) = RoomInfo(
name = dto.name,
capacity = dto.utilities.firstOrNull { it.name == "place" }?.count ?: 0,
isHaveTv = dto.utilities.any { it.name == "tv" },
socketCount = dto.utilities.firstOrNull { it.name == "lan" }?.count ?: 0,
eventList = dto.bookings?.map(eventInfoMapper::map) ?: emptyList(),
currentEvent = null,
id = dto.id
)
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.data.repository
import band.effective.office.tablet.core.data.api.BookingApi
import band.effective.office.tablet.core.data.mapper.EventInfoMapper
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.map
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.repository.BookingRepository
/**
* Implementation of BookingRepository that delegates to NetworkEventRepository.
* This adapter allows the existing implementation to work with the new interfaces.
*/
class BookingRepositoryImpl(
private val api: BookingApi,
private val eventInfoMapper: EventInfoMapper,
) : BookingRepository {
override suspend fun createBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> =
api.createBooking(eventInfoMapper.mapToRequest(eventInfo, room))
.map(errorMapper = { it }, successMapper = eventInfoMapper::map)
override suspend fun updateBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> =
api.updateBooking(eventInfoMapper.mapToRequest(eventInfo, room), eventInfo.id)
.map(errorMapper = { it }, successMapper = eventInfoMapper::map)
override suspend fun deleteBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, String> = api.deleteBooking(eventInfo.id).map(
errorMapper = { it },
successMapper = { "ok" }
)
override suspend fun getBooking(eventInfo: EventInfo): Either<ErrorResponse, EventInfo> {
val response = api.getBooking(eventInfo.id)
return response.map(
errorMapper = { it },
successMapper = eventInfoMapper::map,
)
}
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.data.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.map
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.repository.BookingRepository
import band.effective.office.tablet.core.domain.repository.EventRepositoryMediator
import band.effective.office.tablet.core.domain.repository.LocalBookingRepository
import band.effective.office.tablet.core.domain.unbox
/**
* Default implementation of [EventRepositoryMediator] that coordinates operations between
* network and local repositories.
*
* @property networkRepository Repository for network operations
* @property localRepository Repository for local storage operations
*/
class DefaultEventRepositoryMediator(
private val networkRepository: BookingRepository,
private val localRepository: LocalBookingRepository
) : EventRepositoryMediator {
/**
* Synchronizes data between network and local repositories.
* Fetches data from the network and updates the local repository.
* If the network operation fails, it will use the saved data from the local repository.
*
* @return Either containing the synchronized room information or an error with saved data
*/
override suspend fun synchronizeData(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>> {
val save = localRepository.getRoomsInfo().unbox(
errorHandler = { it.saveData }
)
val roomInfos = networkRepository.getRoomsInfo()
.map(
errorMapper = { error ->
// Prevent NPE by handling null save data
error.copy(saveData = save)
},
successMapper = { it }
)
localRepository.updateRoomsInfo(roomInfos)
return roomInfos
}
/**
* Handles booking creation, coordinating between network and local repositories.
* Updates the local repository immediately with a loading state,
* then attempts to create the booking in the network repository.
* If the network operation fails, the booking is removed from the local repository.
*
* @param eventInfo Information about the event to create
* @param room Information about the room to book
* @return Either containing the created event information or an error
*/
override suspend fun handleBookingCreation(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> {
val loadingEvent = eventInfo.copy(isLoading = true)
// Update local repository with loading state
localRepository.createBooking(loadingEvent, room)
// Attempt to create booking in network repository
val response = networkRepository.createBooking(loadingEvent, room)
when (response) {
is Either.Error -> {
// On error, remove the booking from local repository
localRepository.deleteBooking(loadingEvent, room)
}
is Either.Success -> {
// On success, update the booking in local repository with the response data
val event = response.data
localRepository.updateBooking(event, room)
}
}
return response
}
/**
* Handles booking update, coordinating between network and local repositories.
* Updates the local repository immediately with a loading state,
* then attempts to update the booking in the network repository.
* If the network operation fails, the original event is restored in the local repository.
*
* @param eventInfo Updated information about the event
* @param room Information about the room where the booking exists
* @return Either containing the updated event information or an error
*/
override suspend fun handleBookingUpdate(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> {
val loadingEvent = eventInfo.copy(isLoading = true)
// Get the original event to restore in case of failure
val oldEvent = localRepository.getBooking(eventInfo) as? Either.Success
?: return Either.Error(ErrorResponse(404, "Old event with id ${eventInfo.id} wasn't found"))
// Update local repository with loading state
localRepository.updateBooking(loadingEvent, room)
// Attempt to update booking in network repository
val response = networkRepository.updateBooking(loadingEvent, room)
when (response) {
is Either.Error -> {
// On error, restore the original event in local repository
localRepository.updateBooking(oldEvent.data, room)
}
is Either.Success -> {
// On success, update the booking in local repository with the response data
val event = response.data
localRepository.updateBooking(event, room)
}
}
return response
}
/**
* Handles booking deletion, coordinating between network and local repositories.
* Updates the local repository immediately with a loading state,
* then attempts to delete the booking in the network repository.
* If the network operation fails, the original event is restored in the local repository.
*
* @param eventInfo Information about the event to delete
* @param room Information about the room where the booking exists
* @return Either containing a success message or an error
*/
override suspend fun handleBookingDeletion(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, String> {
val loadingEvent = eventInfo.copy(isLoading = true)
// Save the original event state before attempting to delete
val originalEvent = eventInfo.copy()
// Mark as loading in local repository
localRepository.updateBooking(loadingEvent, room)
// Attempt to delete from network
val response = networkRepository.deleteBooking(loadingEvent, room)
when (response) {
is Either.Error -> {
// On error, restore the original event in local repository
localRepository.updateBooking(originalEvent, room)
}
is Either.Success -> {
// On success, delete from local repository
localRepository.deleteBooking(loadingEvent, room)
}
}
return response
}
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.data.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.repository.BookingRepository
import band.effective.office.tablet.core.domain.repository.EventManagerRepository
import band.effective.office.tablet.core.domain.repository.EventRepositoryMediator
import band.effective.office.tablet.core.domain.repository.LocalBookingRepository
import band.effective.office.tablet.core.domain.unbox
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/**
* Implementation of [EventManagerRepository] that manages events between network and local repositories.
* Handles synchronization, caching, and error recovery for booking operations.
*
* @property networkEventRepository Repository for network operations
* @property localEventStoreRepository Repository for local storage operations
* @property mediator Mediator for coordinating operations between repositories
*/
class EventManagerRepositoryImpl(
private val networkEventRepository: BookingRepository,
private val localEventStoreRepository: LocalBookingRepository,
private val mediator: EventRepositoryMediator = DefaultEventRepositoryMediator(
networkRepository = networkEventRepository,
localRepository = localEventStoreRepository
)
) : EventManagerRepository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val updateJob: Job
init {
updateJob = scope.launch {
networkEventRepository.subscribeOnUpdates().collect {
refreshData()
}
}
}
/**
* Cancels all coroutines launched in this scope.
* Should be called when the EventManager is no longer needed to prevent memory leaks.
*/
fun dispose() {
updateJob.cancel()
scope.cancel()
}
/**
* Returns a flow of room information updates from the local repository.
* @return Flow of Either containing room information or error with saved data
*/
override fun getEventsFlow() = localEventStoreRepository.subscribeOnUpdates()
/**
* Refreshes room information from the network repository and updates the local repository.
* If the network operation fails, it will use the saved data from the local repository.
* @return Either containing the updated room information or error with saved data
*/
override suspend fun refreshData(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>> {
return mediator.synchronizeData()
}
/**
* Creates a new booking in the specified room.
* Updates the local repository immediately with a loading state,
* then attempts to create the booking in the network repository.
* If the network operation fails, the booking is removed from the local repository.
*
* @param roomName Name of the room to book
* @param eventInfo Information about the event to create
* @return Either containing the created event information or an error
*/
override suspend fun createBooking(roomName: String, eventInfo: EventInfo): Either<ErrorResponse, EventInfo> {
val roomInfo = getRoomByName(roomName)
?: return Either.Error(ErrorResponse(404, "Couldn't find a room with name $roomName"))
return mediator.handleBookingCreation(eventInfo, roomInfo)
}
/**
* Updates an existing booking in the specified room.
* Updates the local repository immediately with a loading state,
* then attempts to update the booking in the network repository.
* If the network operation fails, the original event is restored in the local repository.
*
* @param roomName Name of the room where the booking exists
* @param eventInfo Updated information about the event
* @return Either containing the updated event information or an error
*/
override suspend fun updateBooking(roomName: String, eventInfo: EventInfo): Either<ErrorResponse, EventInfo> {
val roomInfo = getRoomByName(roomName)
?: return Either.Error(ErrorResponse(404, "Couldn't find a room with name $roomName"))
return mediator.handleBookingUpdate(eventInfo, roomInfo)
}
/**
* Deletes an existing booking in the specified room.
* Updates the local repository immediately with a loading state,
* then attempts to delete the booking in the network repository.
* If the network operation fails, the original event is restored in the local repository.
*
* @param roomName Name of the room where the booking exists
* @param eventInfo Information about the event to delete
* @return Either containing a success message or an error
*/
override suspend fun deleteBooking(roomName: String, eventInfo: EventInfo): Either<ErrorResponse, String> {
val roomInfo = getRoomByName(roomName)
?: return Either.Error(ErrorResponse(404, "Couldn't find a room with name $roomName"))
return mediator.handleBookingDeletion(eventInfo, roomInfo)
}
/**
* Gets information about all rooms.
* First tries to get the information from the local repository.
* If the local repository has no data, it refreshes the data from the network repository.
*
* @return Either containing room information or an error with saved data
*/
override suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>> {
val roomInfos = localEventStoreRepository.getRoomsInfo()
if (roomInfos as? Either.Error != null
&& roomInfos.error.saveData.isNullOrEmpty()
) {
return refreshData()
}
return roomInfos
}
/**
* Gets the current information about all rooms from the local repository without refreshing from the network.
* This is useful when you need the most recent locally cached data without network latency.
*
* @return Either containing room information or an error with saved data
*/
override suspend fun getCurrentRoomInfos(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>> {
return localEventStoreRepository.getRoomsInfo()
}
/**
* Gets the names of all available rooms.
* If no rooms are available, returns a list with the default room name.
*
* @return List of room names
*/
override suspend fun getRoomNames(): List<String> {
val rooms = getRoomsInfo().unbox(
errorHandler = { it.saveData }
)
return rooms?.map { it.name } ?: listOf(RoomInfo.defaultValue.name)
}
/**
* Gets information about a specific room by its name.
*
* @param roomName Name of the room to get information about
* @return Room information or null if the room is not found
*/
override suspend fun getRoomByName(roomName: String): RoomInfo? {
val rooms = localEventStoreRepository.getRoomsInfo().unbox(
errorHandler = { it.saveData }
)
val room = rooms?.firstOrNull { it.name == roomName }
return room
}
}
package band.effective.office.tablet.core.data.repository
import band.effective.office.tablet.core.data.utils.Buffer
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.map
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.repository.LocalBookingRepository
import band.effective.office.tablet.core.domain.unbox
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
class LocalEventStoreRepository(
private val timeZone: TimeZone = TimeZone.currentSystemDefault(),
/**
* Implementation of LocalBookingRepository that delegates to LocalEventStoreRepository.
* This adapter allows the existing implementation to work with the new interfaces.
*/
class LocalBookingRepositoryImpl(
private val buffer: Buffer,
) : LocalBookingRepository {
private val clock: Clock = Clock.System
private val buffer = MutableStateFlow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>>(
Either.Error(
ErrorWithData(
error = ErrorResponse.getResponse(400),
saveData = emptyList()
)
)
)
val flow = buffer.asStateFlow()
override fun subscribeOnUpdates(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>> = flow
override fun updateRoomsInfo(either: Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>) {
buffer.update { either }
}
override suspend fun createBooking(
eventInfo: EventInfo,
room: RoomInfo
......@@ -49,6 +25,20 @@ class LocalEventStoreRepository(
return Either.Success(eventInfo)
}
override suspend fun updateBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> {
updateRoomInBuffer(room.name) { events ->
val oldEvent = events.firstOrNull {
it.id == eventInfo.id ||
(it.startTime == eventInfo.startTime && it.finishTime == eventInfo.finishTime)
} ?: return@updateRoomInBuffer events
events - oldEvent + eventInfo
}
return Either.Success(eventInfo)
}
override suspend fun deleteBooking(
eventInfo: EventInfo,
room: RoomInfo
......@@ -58,7 +48,7 @@ class LocalEventStoreRepository(
}
override suspend fun getBooking(eventInfo: EventInfo): Either<ErrorResponse, EventInfo> {
return buffer.value.unbox(
return buffer.state.value.unbox(
errorHandler = { it.saveData }
)?.firstNotNullOfOrNull {
it.eventList.firstOrNull { event -> event.id == eventInfo.id }
......@@ -66,22 +56,8 @@ class LocalEventStoreRepository(
?: Either.Error(ErrorResponse(404, "Couldn't find booking with id ${eventInfo.id}"))
}
override suspend fun updateBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> {
updateRoomInBuffer(room.name) { events ->
val oldEvent = events.firstOrNull {
it.id == eventInfo.id ||
(it.startTime == eventInfo.startTime && it.finishTime == eventInfo.finishTime)
} ?: return@updateRoomInBuffer events
events - oldEvent + eventInfo
}
return Either.Success(eventInfo)
}
private fun updateRoomInBuffer(roomName: String, action: (List<EventInfo>) -> List<EventInfo>) {
buffer.update { either ->
buffer.state.update { either ->
either.map(
errorMapper = {
val updatedRooms = it.saveData?.updateRoom(roomName, action)
......@@ -104,49 +80,4 @@ class LocalEventStoreRepository(
mutableRooms[roomIndex] = room.copy(eventList = newEvents)
return mutableRooms
}
override suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>> {
return buffer.value.map(
errorMapper = {
it.copy(saveData = it.saveData?.map { room -> room.updateCurrentEvent() })
},
successMapper = { it.map { room -> room.updateCurrentEvent() } }
)
}
private fun RoomInfo.removePastEvents(): RoomInfo {
val nowInstant = clock.now()
val filtered = eventList.filter {
it.finishTime.toInstant(timeZone) > nowInstant
}
return copy(eventList = filtered)
}
private fun RoomInfo.updateCurrentEvent(): RoomInfo {
val now = clock.now().removeSeconds()
val current = eventList.firstOrNull {
val start = it.startTime.toInstant(timeZone)
val end = it.finishTime.toInstant(timeZone)
start <= now && end > now && !it.isLoading
} ?: return this
return copy(
eventList = eventList - current,
currentEvent = current
)
}
private fun Instant.removeSeconds(): Instant {
val local = this.toLocalDateTime(timeZone)
val rounded = LocalDateTime(
year = local.year,
month = local.month,
dayOfMonth = local.dayOfMonth,
hour = local.hour,
minute = local.minute,
second = 0,
nanosecond = 0
)
return rounded.toInstant(timeZone)
}
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.data.repository
import band.effective.office.tablet.core.data.utils.Buffer
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.map
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.repository.LocalRoomRepository
import band.effective.office.tablet.core.domain.util.asInstant
import band.effective.office.tablet.core.domain.util.cropSeconds
import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.update
/**
* Implementation of LocalRoomRepository that delegates to LocalEventStoreRepository.
* This adapter allows the existing implementation to work with the new interfaces.
*/
class LocalRoomRepositoryImpl(
private val buffer: Buffer,
) : LocalRoomRepository {
override fun subscribeOnUpdates(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>> = buffer.state
override fun updateRoomsInfo(either: Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>) {
buffer.state.update { either }
}
override suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>> {
return buffer.state.value.map(
errorMapper = {
it.copy(saveData = it.saveData?.map { room -> room.updateCurrentEvent() })
},
successMapper = { it.map { room -> room.updateCurrentEvent() } }
)
}
private fun RoomInfo.updateCurrentEvent(): RoomInfo {
val now = currentLocalDateTime.cropSeconds().asInstant
val current = eventList.firstOrNull {
val start = it.startTime.asInstant
val end = it.finishTime.asInstant
start <= now && end > now && !it.isLoading
} ?: return this
return copy(
eventList = eventList - current,
currentEvent = current
)
}
}
\ Нет новой строки в конце файла
......@@ -2,20 +2,14 @@ package band.effective.office.tablet.core.data.repository
import band.effective.office.tablet.core.data.api.BookingApi
import band.effective.office.tablet.core.data.api.WorkspaceApi
import band.effective.office.tablet.core.data.dto.booking.BookingRequestDTO
import band.effective.office.tablet.core.data.dto.booking.BookingResponseDTO
import band.effective.office.tablet.core.data.dto.workspace.WorkspaceDTO
import band.effective.office.tablet.core.data.utils.Converter.toOrganizer
import band.effective.office.tablet.core.data.mapper.RoomInfoMapper
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.map
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.Organizer
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.repository.BookingRepository
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import band.effective.office.tablet.core.domain.repository.RoomRepository
import band.effective.office.tablet.core.domain.util.asInstant
import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import band.effective.office.tablet.core.domain.util.defaultTimeZone
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
......@@ -23,133 +17,65 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
class NetworkEventRepository(
/**
* Implementation of RoomRepository that delegates to NetworkEventRepository.
* This adapter allows the existing implementation to work with the new interfaces.
*/
class RoomRepositoryImpl(
private val api: BookingApi,
private val workspaceApi: WorkspaceApi,
) : BookingRepository {
private val timeZone: TimeZone = TimeZone.currentSystemDefault()
private val clock: Clock = Clock.System
private val roomInfoMapper: RoomInfoMapper,
) : RoomRepository {
private val scope = CoroutineScope(Dispatchers.IO)
/**
* Gets information about all rooms with their bookings.
* Rounds the current time down to the nearest 15-minute interval for the start time,
* and sets the end time to 14 days from now.
*
* @return Either containing room information or an error with saved data
*/
override fun subscribeOnUpdates(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>> {
return api.subscribeOnBookingsList("", scope)
.map { response ->
when (response) {
is Either.Error -> Either.Error(ErrorWithData(response.error, null))
is Either.Success -> {
// When we receive booking updates, fetch the latest room information
// This is a workaround since we can't directly convert BookingResponseDTO to RoomInfo
val roomsInfo = runCatching { getRoomsInfo() }.getOrNull()
roomsInfo ?: Either.Success(emptyList())
}
}
}
}
override suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>> {
// Get current time
val now = clock.now()
val nowLocalDateTime = now.toLocalDateTime(timeZone)
val now = currentLocalDateTime
// Round down to nearest 15-minute interval
val minutes = nowLocalDateTime.minute
val minutes = now.minute
val roundedMinutes = (minutes / 15) * 15
// Create rounded start time
val roundedStart = LocalDateTime(
year = nowLocalDateTime.year,
month = nowLocalDateTime.month, // Month is 1-based in constructor
dayOfMonth = nowLocalDateTime.dayOfMonth,
hour = nowLocalDateTime.hour,
year = now.year,
month = now.month,
dayOfMonth = now.dayOfMonth,
hour = now.hour,
minute = roundedMinutes,
second = 0,
nanosecond = 0
)
// Set end time to 14 days from now
val finish = now.plus(14, DateTimeUnit.DAY, timeZone)
val finish = now.asInstant.plus(14, DateTimeUnit.DAY, defaultTimeZone)
val response = workspaceApi.getWorkspacesWithBookings(
tag = "meeting",
freeFrom = roundedStart.toInstant(timeZone).toEpochMilliseconds(),
freeFrom = roundedStart.asInstant.toEpochMilliseconds(),
freeUntil = finish.toEpochMilliseconds()
)
return when (response) {
is Either.Error -> Either.Error(ErrorWithData(response.error, null))
is Either.Success -> Either.Success(response.data.map { it.toRoom() })
is Either.Success -> Either.Success(response.data.map(roomInfoMapper::map))
}
}
override suspend fun createBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> =
api.createBooking(eventInfo.toBookingRequestDTO(room))
.map(errorMapper = { it }, successMapper = { it.toEventInfo() })
override suspend fun updateBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo> =
api.updateBooking(eventInfo.toBookingRequestDTO(room), eventInfo.id)
.map(errorMapper = { it }, successMapper = { it.toEventInfo() })
override suspend fun deleteBooking(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, String> = api.deleteBooking(eventInfo.id).map(
errorMapper = { it },
successMapper = { "ok" }
)
override suspend fun getBooking(eventInfo: EventInfo): Either<ErrorResponse, EventInfo> {
val response = api.getBooking(eventInfo.id)
return response.map(
errorMapper = { it },
successMapper = { it.toEventInfo() }
)
}
override fun subscribeOnUpdates(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>> =
api.subscribeOnBookingsList("", scope)
.map { response ->
when (response) {
is Either.Error -> Either.Error(ErrorWithData(response.error, null))
is Either.Success -> {
// When we receive booking updates, fetch the latest room information
// This is a workaround since we can't directly convert BookingResponseDTO to RoomInfo
val roomsInfo = runCatching { getRoomsInfo() }.getOrNull()
roomsInfo ?: Either.Success(emptyList())
}
}
}
/** Map domain model to DTO */
private fun EventInfo.toBookingRequestDTO(room: RoomInfo): BookingRequestDTO = BookingRequestDTO(
beginBooking = this.startTime.toInstant(timeZone).toEpochMilliseconds(),
endBooking = this.finishTime.toInstant(timeZone).toEpochMilliseconds(),
ownerEmail = this.organizer.email,
participantEmails = listOfNotNull(this.organizer.email),
workspaceId = room.id
)
/** Map DTO to domain model */
private fun BookingResponseDTO.toEventInfo(): EventInfo = EventInfo(
id = id,
startTime = Instant.fromEpochMilliseconds(beginBooking).toLocalDateTime(timeZone),
finishTime = Instant.fromEpochMilliseconds(endBooking).toLocalDateTime(timeZone),
organizer = owner?.toOrganizer() ?: Organizer.default,
isLoading = false,
)
private fun WorkspaceDTO.toRoom(): RoomInfo =
RoomInfo(
name = name,
capacity = utilities.firstOrNull { it.name == "place" }?.count ?: 0,
isHaveTv = utilities.any { it.name == "tv" },
socketCount = utilities.firstOrNull { it.name == "lan" }?.count ?: 0,
eventList = bookings?.map { it.toEventInfo() } ?: emptyList(),
currentEvent = null,
id = id
)
}
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.data.utils
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.model.RoomInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class Buffer<T>(private val defaultValue: T, private val getValue: suspend () -> T) {
private val buffer: MutableStateFlow<T> = MutableStateFlow(defaultValue)
val bufferFlow = buffer.asStateFlow()
suspend fun bufferedValue(): T {
if (buffer.value == defaultValue) return freshValue()
return buffer.value
}
suspend fun freshValue(): T {
val newValue = getValue()
buffer.emit(newValue)
return newValue
}
suspend fun refresh() {
buffer.emit(getValue())
}
fun update(value: T) {
buffer.update { value }
}
class Buffer {
val state = MutableStateFlow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>>(
Either.Error(
ErrorWithData(
error = ErrorResponse.getResponse(400),
saveData = emptyList()
)
)
)
}
\ Нет новой строки в конце файла
......@@ -4,7 +4,14 @@ import band.effective.office.tablet.core.domain.useCase.CheckBookingUseCase
import band.effective.office.tablet.core.domain.useCase.CheckSettingsUseCase
import band.effective.office.tablet.core.domain.useCase.CreateBookingUseCase
import band.effective.office.tablet.core.domain.useCase.DeleteBookingUseCase
import band.effective.office.tablet.core.domain.useCase.GetCurrentRoomInfosUseCase
import band.effective.office.tablet.core.domain.useCase.GetEventsFlowUseCase
import band.effective.office.tablet.core.domain.useCase.GetRoomByNameUseCase
import band.effective.office.tablet.core.domain.useCase.GetRoomNamesUseCase
import band.effective.office.tablet.core.domain.useCase.GetRoomsInfoUseCase
import band.effective.office.tablet.core.domain.useCase.OrganizersInfoUseCase
import band.effective.office.tablet.core.domain.useCase.RefreshDataUseCase
import band.effective.office.tablet.core.domain.useCase.ResourceDisposerUseCase
import band.effective.office.tablet.core.domain.useCase.RoomInfoUseCase
import band.effective.office.tablet.core.domain.useCase.SelectRoomUseCase
import band.effective.office.tablet.core.domain.useCase.SetRoomUseCase
......@@ -15,8 +22,50 @@ import band.effective.office.tablet.core.domain.useCase.UpdateUseCase
import org.koin.dsl.module
val domainModule = module {
// Use cases
single { RoomInfoUseCase(eventManager = get()) }
// Booking use cases
single {
CreateBookingUseCase(
networkBookingRepository = get(),
localBookingRepository = get(),
getRoomByNameUseCase = get(),
)
}
single {
UpdateBookingUseCase(
networkBookingRepository = get(),
localBookingRepository = get(),
getRoomByNameUseCase = get(),
)
}
single {
DeleteBookingUseCase(
networkBookingRepository = get(),
localBookingRepository = get(),
getRoomByNameUseCase = get(),
)
}
// Room information use cases
single { GetEventsFlowUseCase(localRoomRepository = get()) }
single { RefreshDataUseCase(networkRoomRepository = get(), localRoomRepository = get()) }
single { GetCurrentRoomInfosUseCase(localRoomRepository = get()) }
single { GetRoomByNameUseCase(localRoomRepository = get()) }
single { GetRoomsInfoUseCase(localRoomRepository = get(), refreshDataUseCase = get()) }
single { GetRoomNamesUseCase(getRoomsInfoUseCase = get()) }
single { ResourceDisposerUseCase(networkRoomRepository = get(), refreshDataUseCase = get()) }
single {
RoomInfoUseCase(
getRoomNamesUseCase = get(),
refreshDataUseCase = get(),
getRoomsInfoUseCase = get(),
getEventsFlowUseCase = get(),
getRoomByNameUseCase = get(),
getCurrentRoomInfosUseCase = get(),
)
}
// Other use cases
single { OrganizersInfoUseCase(repository = get()) }
single { CheckBookingUseCase(roomInfoUseCase = get()) }
single { CheckSettingsUseCase() }
......@@ -25,8 +74,4 @@ val domainModule = module {
single { SlotUseCase() }
single { TimerUseCase() }
single { UpdateUseCase(roomInfoUseCase = get(), timerUseCase = get()) }
single { CreateBookingUseCase(get()) }
single { UpdateBookingUseCase(get()) }
single { DeleteBookingUseCase(get()) }
}
\ Нет новой строки в конце файла
}
......@@ -4,6 +4,9 @@ import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
/**
* Domain model representing an event or booking.
*/
@Serializable
data class EventInfo(
val startTime: LocalDateTime,
......@@ -16,6 +19,7 @@ data class EventInfo(
// Validate that start time is before finish time
require(startTime <= finishTime) { "Start time must be before or equal to finish time" }
}
companion object {
const val defaultId: String = ""
......
......@@ -2,6 +2,9 @@ package band.effective.office.tablet.core.domain.model
import kotlinx.serialization.Serializable
/**
* Domain model representing a room.
*/
@Serializable
data class RoomInfo(
val name: String,
......
......@@ -2,33 +2,46 @@ package band.effective.office.tablet.core.domain.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import kotlinx.coroutines.flow.Flow
/**Repository for booking room*/
/**
* Repository interface for booking operations.
* Provides methods for creating, updating, and deleting bookings.
*/
interface BookingRepository {
/**Create booking
* @param eventInfo info about new event
* @param room booking room name
* @return if booking is created - [EventInfo], else - [ErrorResponse]*/
/**
* Creates a new booking in the specified room.
*
* @param eventInfo Information about the event to create
* @param room Information about the room to book
* @return Either containing the created event information or an error
*/
suspend fun createBooking(eventInfo: EventInfo, room: RoomInfo): Either<ErrorResponse, EventInfo>
/**Update booking
* @param eventInfo new info about event
* @param room booking room name
* @return if booking is updated - [EventInfo], else - [ErrorResponse]*/
/**
* Updates an existing booking in the specified room.
*
* @param eventInfo Updated information about the event
* @param room Information about the room where the booking exists
* @return Either containing the updated event information or an error
*/
suspend fun updateBooking(eventInfo: EventInfo, room: RoomInfo): Either<ErrorResponse, EventInfo>
/**Update booking
* @param eventInfo new info about event
* @return if booking is updated - "ok", else - [ErrorResponse]*/
/**
* Deletes an existing booking in the specified room.
*
* @param eventInfo Information about the event to delete
* @param room Information about the room where the booking exists
* @return Either containing a success message or an error
*/
suspend fun deleteBooking(eventInfo: EventInfo, room: RoomInfo): Either<ErrorResponse, String>
/**
* Gets information about a specific booking.
*
* @param eventInfo Information about the event to retrieve
* @return Either containing the event information or an error
*/
suspend fun getBooking(eventInfo: EventInfo): Either<ErrorResponse, EventInfo>
fun subscribeOnUpdates(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>>
suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
}
package band.effective.office.tablet.core.domain.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import kotlinx.coroutines.flow.Flow
interface EventManagerRepository {
fun getEventsFlow(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>>
suspend fun refreshData(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
suspend fun createBooking(roomName: String, eventInfo: EventInfo): Either<ErrorResponse, EventInfo>
suspend fun updateBooking(roomName: String, eventInfo: EventInfo): Either<ErrorResponse, EventInfo>
suspend fun deleteBooking(roomName: String, eventInfo: EventInfo): Either<ErrorResponse, String>
suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
suspend fun getCurrentRoomInfos(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
suspend fun getRoomNames(): List<String>
suspend fun getRoomByName(roomName: String): RoomInfo?
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.domain.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
/**
* Mediator interface for coordinating operations between network and local repositories.
* This interface abstracts the coordination logic, reducing tight coupling between repositories.
*/
interface EventRepositoryMediator {
/**
* Synchronizes data between network and local repositories.
* @return Either containing the synchronized room information or an error with saved data
*/
suspend fun synchronizeData(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
/**
* Handles booking creation, coordinating between network and local repositories.
* @param eventInfo Information about the event to create
* @param room Information about the room to book
* @return Either containing the created event information or an error
*/
suspend fun handleBookingCreation(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo>
/**
* Handles booking update, coordinating between network and local repositories.
* @param eventInfo Updated information about the event
* @param room Information about the room where the booking exists
* @return Either containing the updated event information or an error
*/
suspend fun handleBookingUpdate(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, EventInfo>
/**
* Handles booking deletion, coordinating between network and local repositories.
* @param eventInfo Information about the event to delete
* @param room Information about the room where the booking exists
* @return Either containing a success message or an error
*/
suspend fun handleBookingDeletion(
eventInfo: EventInfo,
room: RoomInfo
): Either<ErrorResponse, String>
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.core.domain.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.ErrorResponse
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
interface LocalBookingRepository : BookingRepository {
fun updateRoomsInfo(either: Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>)
}
\ Нет новой строки в конце файла
/**
* Repository interface for local booking operations.
* Extends [BookingRepository] to provide additional methods for local storage operations.
*/
interface LocalBookingRepository {
/**
* Creates a new booking in the specified room.
*
* @param eventInfo Information about the event to create
* @param room Information about the room to book
* @return Either containing the created event information or an error
*/
suspend fun createBooking(eventInfo: EventInfo, room: RoomInfo): Either<ErrorResponse, EventInfo>
/**
* Updates an existing booking in the specified room.
*
* @param eventInfo Updated information about the event
* @param room Information about the room where the booking exists
* @return Either containing the updated event information or an error
*/
suspend fun updateBooking(eventInfo: EventInfo, room: RoomInfo): Either<ErrorResponse, EventInfo>
/**
* Deletes an existing booking in the specified room.
*
* @param eventInfo Information about the event to delete
* @param room Information about the room where the booking exists
* @return Either containing a success message or an error
*/
suspend fun deleteBooking(eventInfo: EventInfo, room: RoomInfo): Either<ErrorResponse, String>
/**
* Gets information about a specific booking.
*
* @param eventInfo Information about the event to retrieve
* @return Either containing the event information or an error
*/
suspend fun getBooking(eventInfo: EventInfo): Either<ErrorResponse, EventInfo>
}
package band.effective.office.tablet.core.domain.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.model.RoomInfo
import kotlinx.coroutines.flow.Flow
/**
* Repository interface for local room-related operations.
* Extends [RoomRepository] to provide additional methods for local storage operations.
*/
interface LocalRoomRepository {
/**
* Updates the local storage with room information.
* This method is typically called after fetching data from a remote source.
*
* @param either Either containing room information or an error with saved data
*/
fun updateRoomsInfo(either: Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>)
/**
* Subscribes to updates about rooms and their bookings.
*
* @return Flow of Either containing room information or an error with saved data
*/
fun subscribeOnUpdates(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>>
/**
* Gets information about all rooms with their bookings.
*
* @return Either containing room information or an error with saved data
*/
suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
}
\ Нет новой строки в конце файла
......@@ -3,17 +3,24 @@ package band.effective.office.tablet.core.domain.repository
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.model.RoomInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
/**Repository for get information about room*/
/**
* Repository interface for room-related operations.
* Provides methods for retrieving information about rooms and their bookings.
*/
interface RoomRepository {
/**Get list all rooms*/
suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
fun subscribeOnUpdates(
scope: CoroutineScope
): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>>
/**
* Subscribes to updates about rooms and their bookings.
*
* @return Flow of Either containing room information or an error with saved data
*/
fun subscribeOnUpdates(): Flow<Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>>
suspend fun updateCashe()
/**
* Gets information about all rooms with their bookings.
*
* @return Either containing room information or an error with saved data
*/
suspend fun getRoomsInfo(): Either<ErrorWithData<List<RoomInfo>>, List<RoomInfo>>
}
\ Нет новой строки в конце файла
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать