mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-02 12:36:44 +01:00
commit
d2891cac69
@ -171,7 +171,7 @@ internal class FeatureModules internal constructor(
|
|||||||
coroutineDispatchers
|
coroutineDispatchers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync) }
|
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room) }
|
||||||
val notificationsModule by unsafeLazy {
|
val notificationsModule by unsafeLazy {
|
||||||
NotificationsModule(
|
NotificationsModule(
|
||||||
matrixModules.push,
|
matrixModules.push,
|
||||||
|
@ -27,14 +27,13 @@ fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Toolbar(
|
if (currentPage.hasToolbar) {
|
||||||
onNavigate = navigateAndPopStack,
|
Toolbar(
|
||||||
title = currentPage.label
|
onNavigate = navigateAndPopStack,
|
||||||
)
|
title = currentPage.label
|
||||||
|
)
|
||||||
currentPage.parent?.let {
|
|
||||||
BackHandler(onBack = navigateAndPopStack)
|
|
||||||
}
|
}
|
||||||
|
BackHandler(onBack = navigateAndPopStack)
|
||||||
computedWeb[currentPage.route]!!.invoke(currentPage.state)
|
computedWeb[currentPage.route]!!.invoke(currentPage.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null) {
|
fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null, body: @Composable () -> Unit = {}) {
|
||||||
val modifier = Modifier.padding(horizontal = 24.dp)
|
val modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
@ -31,6 +31,7 @@ fun TextRow(title: String, content: String? = null, includeDivider: Boolean = tr
|
|||||||
Text(text = content, fontSize = 18.sp)
|
Text(text = content, fontSize = 18.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
body()
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
if (includeDivider) {
|
if (includeDivider) {
|
||||||
|
@ -11,10 +11,8 @@ import app.dapk.st.matrix.sync.RoomInvite
|
|||||||
import app.dapk.st.matrix.sync.RoomOverview
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
private val json = Json
|
private val json = Json
|
||||||
@ -31,11 +29,22 @@ internal class OverviewPersistence(
|
|||||||
.map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
|
.map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun removeRooms(roomsToRemove: List<RoomId>) {
|
||||||
|
dispatchers.withIoContext {
|
||||||
|
database.transaction {
|
||||||
|
roomsToRemove.forEach {
|
||||||
|
database.inviteStateQueries.remove(it.value)
|
||||||
|
database.overviewStateQueries.remove(it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun persistInvites(invites: List<RoomInvite>) {
|
override suspend fun persistInvites(invites: List<RoomInvite>) {
|
||||||
dispatchers.withIoContext {
|
dispatchers.withIoContext {
|
||||||
database.inviteStateQueries.transaction {
|
database.inviteStateQueries.transaction {
|
||||||
invites.forEach {
|
invites.forEach {
|
||||||
database.inviteStateQueries.insert(it.roomId.value)
|
database.inviteStateQueries.insert(it.roomId.value, json.encodeToString(RoomInvite.serializer(), it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,7 +54,15 @@ internal class OverviewPersistence(
|
|||||||
return database.inviteStateQueries.selectAll()
|
return database.inviteStateQueries.selectAll()
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToList()
|
.mapToList()
|
||||||
.map { it.map { RoomInvite(RoomId(it)) } }
|
.map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeInvites(invites: List<RoomId>) {
|
||||||
|
dispatchers.withIoContext {
|
||||||
|
database.inviteStateQueries.transaction {
|
||||||
|
invites.forEach { database.inviteStateQueries.remove(it.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun persist(overviewState: OverviewState) {
|
override suspend fun persist(overviewState: OverviewState) {
|
||||||
@ -59,7 +76,7 @@ internal class OverviewPersistence(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun retrieve(): OverviewState {
|
override suspend fun retrieve(): OverviewState {
|
||||||
return withContext(Dispatchers.IO) {
|
return dispatchers.withIoContext {
|
||||||
val overviews = database.overviewStateQueries.selectAll().executeAsList()
|
val overviews = database.overviewStateQueries.selectAll().executeAsList()
|
||||||
overviews.map { json.decodeFromString(RoomOverview.serializer(), it.blob) }
|
overviews.map { json.decodeFromString(RoomOverview.serializer(), it.blob) }
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,13 @@ internal class RoomPersistence(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(rooms: List<RoomId>) {
|
||||||
|
coroutineDispatchers
|
||||||
|
database.roomEventQueries.transaction {
|
||||||
|
rooms.forEach { database.roomEventQueries.remove(it.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun latest(roomId: RoomId): Flow<RoomState> {
|
override fun latest(roomId: RoomId): Flow<RoomState> {
|
||||||
val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map {
|
val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map {
|
||||||
json.decodeFromString(RoomOverview.serializer(), it)
|
json.decodeFromString(RoomOverview.serializer(), it)
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
CREATE TABLE dbInviteState (
|
CREATE TABLE dbInviteState (
|
||||||
room_id TEXT NOT NULL,
|
room_id TEXT NOT NULL,
|
||||||
|
blob TEXT NOT NULL,
|
||||||
PRIMARY KEY (room_id)
|
PRIMARY KEY (room_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
selectAll:
|
selectAll:
|
||||||
SELECT room_id
|
SELECT room_id, blob
|
||||||
FROM dbInviteState;
|
FROM dbInviteState;
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
INSERT OR REPLACE INTO dbInviteState(room_id)
|
INSERT OR REPLACE INTO dbInviteState(room_id, blob)
|
||||||
VALUES (?);
|
VALUES (?, ?);
|
||||||
|
|
||||||
|
remove:
|
||||||
|
DELETE FROM dbInviteState
|
||||||
|
WHERE room_id = ?;
|
@ -18,4 +18,8 @@ WHERE room_id = ?;
|
|||||||
|
|
||||||
insert:
|
insert:
|
||||||
INSERT OR REPLACE INTO dbOverviewState(room_id, latest_activity_timestamp_utc, read_marker, blob)
|
INSERT OR REPLACE INTO dbOverviewState(room_id, latest_activity_timestamp_utc, read_marker, blob)
|
||||||
VALUES (?, ?, ?, ?);
|
VALUES (?, ?, ?, ?);
|
||||||
|
|
||||||
|
remove:
|
||||||
|
DELETE FROM dbOverviewState
|
||||||
|
WHERE room_id = ?;
|
@ -30,4 +30,10 @@ WHERE event_id = ?;
|
|||||||
selectAllUnread:
|
selectAllUnread:
|
||||||
SELECT dbRoomEvent.blob, dbRoomEvent.room_id
|
SELECT dbRoomEvent.blob, dbRoomEvent.room_id
|
||||||
FROM dbUnreadEvent
|
FROM dbUnreadEvent
|
||||||
INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id;
|
INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id
|
||||||
|
ORDER BY dbRoomEvent.timestamp_utc DESC
|
||||||
|
LIMIT 100;
|
||||||
|
|
||||||
|
remove:
|
||||||
|
DELETE FROM dbRoomEvent
|
||||||
|
WHERE room_id = ?;
|
@ -1,6 +1,5 @@
|
|||||||
package app.dapk.st.home
|
package app.dapk.st.home
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -38,8 +37,9 @@ fun HomeScreen(homeViewModel: HomeViewModel) {
|
|||||||
when (state.page) {
|
when (state.page) {
|
||||||
Directory -> DirectoryScreen(homeViewModel.directory())
|
Directory -> DirectoryScreen(homeViewModel.directory())
|
||||||
Profile -> {
|
Profile -> {
|
||||||
BackHandler { homeViewModel.changePage(Directory) }
|
ProfileScreen(homeViewModel.profile()) {
|
||||||
ProfileScreen(homeViewModel.profile())
|
homeViewModel.changePage(Directory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ class NotificationRenderer(
|
|||||||
private val notificationFactory: NotificationFactory,
|
private val notificationFactory: NotificationFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun render(result: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>) {
|
suspend fun render(result: Map<RoomOverview, List<RoomEvent>>, removedRooms: Set<RoomId>, onlyContainsRemovals: Boolean) {
|
||||||
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
|
removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) }
|
||||||
val notifications = notificationFactory.createNotifications(result)
|
val notifications = notificationFactory.createNotifications(result)
|
||||||
|
|
||||||
@ -26,7 +26,11 @@ class NotificationRenderer(
|
|||||||
notifications.delegates.forEach {
|
notifications.delegates.forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
|
is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID)
|
||||||
is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
|
is NotificationDelegate.Room -> {
|
||||||
|
if (!onlyContainsRemovals) {
|
||||||
|
notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package app.dapk.st.notifications
|
|||||||
import app.dapk.st.core.AppLogTag.NOTIFICATION
|
import app.dapk.st.core.AppLogTag.NOTIFICATION
|
||||||
import app.dapk.st.core.log
|
import app.dapk.st.core.log
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
import app.dapk.st.matrix.sync.RoomStore
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@ -14,7 +15,7 @@ class NotificationsUseCase(
|
|||||||
notificationChannels: NotificationChannels,
|
notificationChannels: NotificationChannels,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val inferredCurrentNotifications = mutableSetOf<RoomId>()
|
private val inferredCurrentNotifications = mutableMapOf<RoomId, List<RoomEvent>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
notificationChannels.initChannels()
|
notificationChannels.initChannels()
|
||||||
@ -26,13 +27,17 @@ class NotificationsUseCase(
|
|||||||
.onEach { result ->
|
.onEach { result ->
|
||||||
log(NOTIFICATION, "unread changed - render notifications")
|
log(NOTIFICATION, "unread changed - render notifications")
|
||||||
|
|
||||||
val asRooms = result.keys.map { it.roomId }.toSet()
|
val changes = result.mapKeys { it.key.roomId }
|
||||||
val removedRooms = inferredCurrentNotifications - asRooms
|
|
||||||
|
|
||||||
|
val asRooms = changes.keys
|
||||||
|
val removedRooms = inferredCurrentNotifications.keys - asRooms
|
||||||
|
|
||||||
|
val onlyContainsRemovals =
|
||||||
|
inferredCurrentNotifications.filterKeys { !removedRooms.contains(it) } == changes.filterKeys { !removedRooms.contains(it) }
|
||||||
inferredCurrentNotifications.clear()
|
inferredCurrentNotifications.clear()
|
||||||
inferredCurrentNotifications.addAll(asRooms)
|
inferredCurrentNotifications.putAll(changes)
|
||||||
|
|
||||||
notificationRenderer.render(result, removedRooms)
|
notificationRenderer.render(result, removedRooms, onlyContainsRemovals)
|
||||||
}
|
}
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,10 @@ class PushAndroidService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
|
log(PUSH, "new push token received")
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
module.pushUseCase().registerPush(token)
|
module.pushUseCase().registerPush(token)
|
||||||
|
log(PUSH, "token registered")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ applyAndroidLibraryModule(project)
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:sync")
|
implementation project(":matrix:services:sync")
|
||||||
|
implementation project(":matrix:services:room")
|
||||||
implementation project(":matrix:services:profile")
|
implementation project(":matrix:services:profile")
|
||||||
implementation project(":features:settings")
|
implementation project(":features:settings")
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
|
@ -2,15 +2,17 @@ package app.dapk.st.profile
|
|||||||
|
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
import app.dapk.st.matrix.room.ProfileService
|
||||||
|
import app.dapk.st.matrix.room.RoomService
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
|
|
||||||
class ProfileModule(
|
class ProfileModule(
|
||||||
private val profileService: ProfileService,
|
private val profileService: ProfileService,
|
||||||
private val syncService: SyncService,
|
private val syncService: SyncService,
|
||||||
|
private val roomService: RoomService,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun profileViewModel(): ProfileViewModel {
|
fun profileViewModel(): ProfileViewModel {
|
||||||
return ProfileViewModel(profileService, syncService)
|
return ProfileViewModel(profileService, syncService, roomService)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,12 +1,13 @@
|
|||||||
package app.dapk.st.profile
|
package app.dapk.st.profile
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.IconButton
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CameraAlt
|
import androidx.compose.material.icons.filled.CameraAlt
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
@ -16,25 +17,44 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.LifecycleEffect
|
import app.dapk.st.core.LifecycleEffect
|
||||||
import app.dapk.st.core.StartObserving
|
import app.dapk.st.core.StartObserving
|
||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.design.components.CircleishAvatar
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.design.components.Spider
|
import app.dapk.st.matrix.sync.InviteMeta
|
||||||
import app.dapk.st.design.components.TextRow
|
import app.dapk.st.matrix.sync.RoomInvite
|
||||||
import app.dapk.st.design.components.percentOfHeight
|
|
||||||
import app.dapk.st.settings.SettingsActivity
|
import app.dapk.st.settings.SettingsActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(viewModel: ProfileViewModel) {
|
fun ProfileScreen(viewModel: ProfileViewModel, onTopLevelBack: () -> Unit) {
|
||||||
viewModel.ObserveEvents()
|
viewModel.ObserveEvents()
|
||||||
|
|
||||||
LifecycleEffect(onStart = {
|
LifecycleEffect(
|
||||||
viewModel.start()
|
onStart = { viewModel.start() },
|
||||||
})
|
onStop = { viewModel.stop() }
|
||||||
|
)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val onNavigate: (SpiderPage<out Page>?) -> Unit = {
|
||||||
|
when (it) {
|
||||||
|
null -> onTopLevelBack()
|
||||||
|
else -> viewModel.goTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
||||||
|
item(Page.Routes.profile) {
|
||||||
|
ProfilePage(context, viewModel, it)
|
||||||
|
}
|
||||||
|
item(Page.Routes.invitation) {
|
||||||
|
Invitations(viewModel, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: Page.Profile) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -45,20 +65,20 @@ fun ProfileScreen(viewModel: ProfileViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val state = viewModel.state) {
|
when (val state = profile.content) {
|
||||||
ProfileScreenState.Loading -> CenteredLoading()
|
is Lce.Loading -> CenteredLoading()
|
||||||
is ProfileScreenState.Content -> {
|
is Lce.Content -> {
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
|
val content = state.value
|
||||||
Column {
|
Column {
|
||||||
Spacer(modifier = Modifier.fillMaxHeight(0.05f))
|
Spacer(modifier = Modifier.fillMaxHeight(0.05f))
|
||||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
val fallbackLabel = state.me.displayName ?: state.me.userId.value
|
val fallbackLabel = content.me.displayName ?: content.me.userId.value
|
||||||
val avatarSize = configuration.percentOfHeight(0.2f)
|
val avatarSize = configuration.percentOfHeight(0.2f)
|
||||||
Box {
|
Box {
|
||||||
CircleishAvatar(state.me.avatarUrl?.value, fallbackLabel, avatarSize)
|
CircleishAvatar(content.me.avatarUrl?.value, fallbackLabel, avatarSize)
|
||||||
|
|
||||||
// TODO enable once edit support it added
|
// TODO enable once edit support is added
|
||||||
if (false) {
|
if (false) {
|
||||||
IconButton(modifier = Modifier
|
IconButton(modifier = Modifier
|
||||||
.size(avatarSize * 0.314f)
|
.size(avatarSize * 0.314f)
|
||||||
@ -76,26 +96,61 @@ fun ProfileScreen(viewModel: ProfileViewModel) {
|
|||||||
|
|
||||||
TextRow(
|
TextRow(
|
||||||
title = "Display name",
|
title = "Display name",
|
||||||
content = state.me.displayName ?: "Not set",
|
content = content.me.displayName ?: "Not set",
|
||||||
)
|
)
|
||||||
TextRow(
|
TextRow(
|
||||||
title = "User id",
|
title = "User id",
|
||||||
content = state.me.userId.value,
|
content = content.me.userId.value,
|
||||||
)
|
)
|
||||||
TextRow(
|
TextRow(
|
||||||
title = "Homeserver",
|
title = "Homeserver",
|
||||||
content = state.me.homeServerUrl.value,
|
content = content.me.homeServerUrl.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
TextRow(
|
TextRow(
|
||||||
title = "Invitations",
|
title = "Invitations",
|
||||||
content = "${state.invitationsCount} pending",
|
content = "${content.invitationsCount} pending",
|
||||||
|
onClick = { viewModel.goToInvitations() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
|
||||||
|
when (val state = invitations.content) {
|
||||||
|
is Lce.Loading -> CenteredLoading()
|
||||||
|
is Lce.Content -> {
|
||||||
|
LazyColumn {
|
||||||
|
items(state.value) {
|
||||||
|
val text = when (val meta = it.inviteMeta) {
|
||||||
|
InviteMeta.DirectMessage -> "${it.inviterName()} has invited you to chat"
|
||||||
|
is InviteMeta.Room -> "${it.inviterName()} has invited you to ${meta.roomName ?: "unnamed room"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow(title = text, includeDivider = false) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row {
|
||||||
|
Button(modifier = Modifier.weight(1f), onClick = { viewModel.rejectRoomInvite(it.roomId) }) {
|
||||||
|
Text("Reject".uppercase())
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.fillMaxWidth(0.1f))
|
||||||
|
Button(modifier = Modifier.weight(1f), onClick = { viewModel.acceptRoomInvite(it.roomId) }) {
|
||||||
|
Text("Accept".uppercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Lce.Error -> TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProfileViewModel.ObserveEvents() {
|
private fun ProfileViewModel.ObserveEvents() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -1,14 +1,30 @@
|
|||||||
package app.dapk.st.profile
|
package app.dapk.st.profile
|
||||||
|
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.design.components.Route
|
||||||
|
import app.dapk.st.design.components.SpiderPage
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
import app.dapk.st.matrix.room.ProfileService
|
||||||
|
import app.dapk.st.matrix.sync.RoomInvite
|
||||||
|
|
||||||
sealed interface ProfileScreenState {
|
data class ProfileScreenState(
|
||||||
object Loading : ProfileScreenState
|
val page: SpiderPage<out Page>,
|
||||||
data class Content(
|
)
|
||||||
val me: ProfileService.Me,
|
|
||||||
val invitationsCount: Int,
|
|
||||||
) : ProfileScreenState
|
|
||||||
|
|
||||||
|
sealed interface Page {
|
||||||
|
data class Profile(val content: Lce<Content>) : Page {
|
||||||
|
data class Content(
|
||||||
|
val me: ProfileService.Me,
|
||||||
|
val invitationsCount: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Invitations(val content: Lce<List<RoomInvite>>): Page
|
||||||
|
|
||||||
|
object Routes {
|
||||||
|
val profile = Route<Profile>("Profile")
|
||||||
|
val invitation = Route<Invitations>("Invitations")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ProfileEvent {
|
sealed interface ProfileEvent {
|
||||||
|
@ -1,27 +1,74 @@
|
|||||||
package app.dapk.st.profile
|
package app.dapk.st.profile
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.design.components.SpiderPage
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
import app.dapk.st.matrix.room.ProfileService
|
||||||
|
import app.dapk.st.matrix.room.RoomService
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ProfileViewModel(
|
class ProfileViewModel(
|
||||||
private val profileService: ProfileService,
|
private val profileService: ProfileService,
|
||||||
private val syncService: SyncService,
|
private val syncService: SyncService,
|
||||||
|
private val roomService: RoomService,
|
||||||
) : DapkViewModel<ProfileScreenState, ProfileEvent>(
|
) : DapkViewModel<ProfileScreenState, ProfileEvent>(
|
||||||
initialState = ProfileScreenState.Loading
|
ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private var syncingJob: Job? = null
|
||||||
|
private var currentPageJob: Job? = null
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
viewModelScope.launch {
|
goToProfile()
|
||||||
val invitationsCount = syncService.invites().firstOrNull()?.size ?: 0
|
}
|
||||||
val me = profileService.me(forceRefresh = true)
|
|
||||||
state = ProfileScreenState.Content(me, invitationsCount = invitationsCount)
|
private fun goToProfile() {
|
||||||
|
syncingJob = syncService.startSyncing().launchIn(viewModelScope)
|
||||||
|
|
||||||
|
combine(
|
||||||
|
flow { emit(profileService.me(forceRefresh = true)) },
|
||||||
|
syncService.invites(),
|
||||||
|
transform = { me, invites -> me to invites }
|
||||||
|
)
|
||||||
|
.onEach { (me, invites) ->
|
||||||
|
updatePageState<Page.Profile> {
|
||||||
|
copy(content = Lce.Content(Page.Profile.Content(me, invites.size)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchPageJob()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun goToInvitations() {
|
||||||
|
updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) }
|
||||||
|
|
||||||
|
syncService.invites()
|
||||||
|
.onEach {
|
||||||
|
updatePageState<Page.Invitations> {
|
||||||
|
copy(content = Lce.Content(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchPageJob()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goTo(page: SpiderPage<out Page>) {
|
||||||
|
currentPageJob?.cancel()
|
||||||
|
updateState { copy(page = page) }
|
||||||
|
when (page.state) {
|
||||||
|
is Page.Invitations -> goToInvitations()
|
||||||
|
is Page.Profile -> goToProfile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> Flow<T>.launchPageJob() {
|
||||||
|
currentPageJob?.cancel()
|
||||||
|
currentPageJob = this.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
fun updateDisplayName() {
|
fun updateDisplayName() {
|
||||||
// TODO
|
// TODO
|
||||||
@ -31,4 +78,40 @@ class ProfileViewModel(
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun acceptRoomInvite(roomId: RoomId) {
|
||||||
|
launchCatching { roomService.joinRoom(roomId) }.fold(
|
||||||
|
onError = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rejectRoomInvite(roomId: RoomId) {
|
||||||
|
launchCatching { roomService.rejectJoinRoom(roomId) }.fold(
|
||||||
|
onError = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
syncingJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private inline fun <reified S : Page> updatePageState(crossinline block: S.() -> S) {
|
||||||
|
val page = state.page
|
||||||
|
val currentState = page.state
|
||||||
|
require(currentState is S)
|
||||||
|
updateState { copy(page = (page as SpiderPage<S>).copy(state = block(page.state))) }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <S, VE, T> DapkViewModel<S, VE>.launchCatching(block: suspend () -> T): LaunchCatching<T> {
|
||||||
|
return object : LaunchCatching<T> {
|
||||||
|
override fun fold(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) {
|
||||||
|
viewModelScope.launch { runCatching { block() }.fold(onSuccess, onError) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LaunchCatching<T> {
|
||||||
|
fun fold(onSuccess: (T) -> Unit = {}, onError: (Throwable) -> Unit = {})
|
||||||
|
}
|
@ -199,9 +199,7 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) {
|
private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) {
|
||||||
Column {
|
Column {
|
||||||
TextRow("Import room keys", includeDivider = false) {
|
TextRow("Import room keys", includeDivider = false, onClick = { viewModel.goToImportRoom() })
|
||||||
viewModel.goToImportRoom()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ interface RoomService : MatrixService {
|
|||||||
suspend fun createDm(userId: UserId, encrypted: Boolean): RoomId
|
suspend fun createDm(userId: UserId, encrypted: Boolean): RoomId
|
||||||
|
|
||||||
suspend fun joinRoom(roomId: RoomId)
|
suspend fun joinRoom(roomId: RoomId)
|
||||||
|
suspend fun rejectJoinRoom(roomId: RoomId)
|
||||||
|
|
||||||
data class JoinedMember(
|
data class JoinedMember(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
|
@ -62,6 +62,10 @@ class DefaultRoomService(
|
|||||||
override suspend fun joinRoom(roomId: RoomId) {
|
override suspend fun joinRoom(roomId: RoomId) {
|
||||||
httpClient.execute(joinRoomRequest(roomId))
|
httpClient.execute(joinRoomRequest(roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun rejectJoinRoom(roomId: RoomId) {
|
||||||
|
httpClient.execute(rejectJoinRoomRequest(roomId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun joinedMembersRequest(roomId: RoomId) = httpRequest<JoinedMembersResponse>(
|
internal fun joinedMembersRequest(roomId: RoomId) = httpRequest<JoinedMembersResponse>(
|
||||||
@ -87,6 +91,13 @@ internal fun joinRoomRequest(roomId: RoomId) = httpRequest<Unit>(
|
|||||||
body = emptyJsonBody()
|
body = emptyJsonBody()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun rejectJoinRoomRequest(roomId: RoomId) = httpRequest<Unit>(
|
||||||
|
path = "_matrix/client/r0/rooms/${roomId.value}/leave",
|
||||||
|
method = MatrixHttpClient.Method.POST,
|
||||||
|
body = emptyJsonBody()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@Suppress("EnumEntryName")
|
@Suppress("EnumEntryName")
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class RoomVisibility {
|
enum class RoomVisibility {
|
||||||
|
@ -31,5 +31,18 @@ data class LastMessage(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RoomInvite(
|
data class RoomInvite(
|
||||||
|
@SerialName("from") val from: RoomMember,
|
||||||
@SerialName("room_id") val roomId: RoomId,
|
@SerialName("room_id") val roomId: RoomId,
|
||||||
|
@SerialName("meta") val inviteMeta: InviteMeta,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class InviteMeta {
|
||||||
|
@Serializable
|
||||||
|
@SerialName("direct_message")
|
||||||
|
object DirectMessage : InviteMeta()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("room")
|
||||||
|
data class Room(val roomName: String? = null) : InviteMeta()
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
interface RoomStore {
|
interface RoomStore {
|
||||||
|
|
||||||
suspend fun persist(roomId: RoomId, state: RoomState)
|
suspend fun persist(roomId: RoomId, state: RoomState)
|
||||||
|
suspend fun remove(rooms: List<RoomId>)
|
||||||
suspend fun retrieve(roomId: RoomId): RoomState?
|
suspend fun retrieve(roomId: RoomId): RoomState?
|
||||||
fun latest(roomId: RoomId): Flow<RoomState>
|
fun latest(roomId: RoomId): Flow<RoomState>
|
||||||
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
|
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
|
||||||
@ -28,6 +29,7 @@ interface FilterStore {
|
|||||||
|
|
||||||
interface OverviewStore {
|
interface OverviewStore {
|
||||||
|
|
||||||
|
suspend fun removeRooms(roomsToRemove: List<RoomId>)
|
||||||
suspend fun persistInvites(invite: List<RoomInvite>)
|
suspend fun persistInvites(invite: List<RoomInvite>)
|
||||||
suspend fun persist(overviewState: OverviewState)
|
suspend fun persist(overviewState: OverviewState)
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ interface OverviewStore {
|
|||||||
|
|
||||||
fun latest(): Flow<OverviewState>
|
fun latest(): Flow<OverviewState>
|
||||||
fun latestInvites(): Flow<List<RoomInvite>>
|
fun latestInvites(): Flow<List<RoomInvite>>
|
||||||
|
suspend fun removeInvites(map: List<RoomId>)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncStore {
|
interface SyncStore {
|
||||||
|
@ -18,7 +18,7 @@ private val SERVICE_KEY = SyncService::class
|
|||||||
|
|
||||||
interface SyncService : MatrixService {
|
interface SyncService : MatrixService {
|
||||||
|
|
||||||
suspend fun invites(): Flow<InviteState>
|
fun invites(): Flow<InviteState>
|
||||||
fun overview(): Flow<OverviewState>
|
fun overview(): Flow<OverviewState>
|
||||||
fun room(roomId: RoomId): Flow<RoomState>
|
fun room(roomId: RoomId): Flow<RoomState>
|
||||||
fun startSyncing(): Flow<Unit>
|
fun startSyncing(): Flow<Unit>
|
||||||
|
@ -63,6 +63,7 @@ internal class DefaultSyncService(
|
|||||||
EphemeralEventsUseCase(roomMembersService, syncEventsFlow),
|
EphemeralEventsUseCase(roomMembersService, syncEventsFlow),
|
||||||
),
|
),
|
||||||
roomRefresher,
|
roomRefresher,
|
||||||
|
roomDataSource,
|
||||||
logger,
|
logger,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers,
|
||||||
)
|
)
|
||||||
@ -100,7 +101,7 @@ internal class DefaultSyncService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun startSyncing() = syncFlow
|
override fun startSyncing() = syncFlow
|
||||||
override suspend fun invites() = overviewStore.latestInvites()
|
override fun invites() = overviewStore.latestInvites()
|
||||||
override fun overview() = overviewStore.latest()
|
override fun overview() = overviewStore.latest()
|
||||||
override fun room(roomId: RoomId) = roomStore.latest(roomId)
|
override fun room(roomId: RoomId) = roomStore.latest(roomId)
|
||||||
override fun events() = syncEventsFlow
|
override fun events() = syncEventsFlow
|
||||||
|
@ -214,6 +214,7 @@ sealed class ApiToDeviceEvent {
|
|||||||
internal data class ApiSyncRooms(
|
internal data class ApiSyncRooms(
|
||||||
@SerialName("join") val join: Map<RoomId, ApiSyncRoom>? = null,
|
@SerialName("join") val join: Map<RoomId, ApiSyncRoom>? = null,
|
||||||
@SerialName("invite") val invite: Map<RoomId, ApiSyncRoomInvite>? = null,
|
@SerialName("invite") val invite: Map<RoomId, ApiSyncRoomInvite>? = null,
|
||||||
|
@SerialName("leave") val leave: Map<RoomId, ApiSyncRoom>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -230,14 +231,30 @@ internal data class ApiInviteEvents(
|
|||||||
sealed class ApiStrippedEvent {
|
sealed class ApiStrippedEvent {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("m.room.create")
|
@SerialName("m.room.member")
|
||||||
internal data class RoomCreate(
|
internal data class RoomMember(
|
||||||
|
@SerialName("content") val content: Content,
|
||||||
|
@SerialName("sender") val sender: UserId,
|
||||||
|
) : ApiStrippedEvent() {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class Content(
|
||||||
|
@SerialName("displayname") val displayName: String? = null,
|
||||||
|
@SerialName("membership") val membership: ApiTimelineEvent.RoomMember.Content.Membership? = null,
|
||||||
|
@SerialName("is_direct") val isDirect: Boolean? = null,
|
||||||
|
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("m.room.name")
|
||||||
|
internal data class RoomName(
|
||||||
@SerialName("content") val content: Content,
|
@SerialName("content") val content: Content,
|
||||||
) : ApiStrippedEvent() {
|
) : ApiStrippedEvent() {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
internal data class Content(
|
internal data class Content(
|
||||||
@SerialName("type") val type: String? = null
|
@SerialName("name") val name: String? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,6 +424,7 @@ internal sealed class ApiTimelineEvent {
|
|||||||
value class Membership(val value: String) {
|
value class Membership(val value: String) {
|
||||||
fun isJoin() = value == "join"
|
fun isJoin() = value == "join"
|
||||||
fun isInvite() = value == "invite"
|
fun isInvite() = value == "invite"
|
||||||
|
fun isLeave() = value == "leave"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ class RoomDataSource(
|
|||||||
|
|
||||||
private val roomCache = mutableMapOf<RoomId, RoomState>()
|
private val roomCache = mutableMapOf<RoomId, RoomState>()
|
||||||
|
|
||||||
|
fun contains(roomId: RoomId) = roomCache.containsKey(roomId)
|
||||||
|
|
||||||
suspend fun read(roomId: RoomId) = when (val cached = roomCache[roomId]) {
|
suspend fun read(roomId: RoomId) = when (val cached = roomCache[roomId]) {
|
||||||
null -> roomStore.retrieve(roomId)?.also { roomCache[roomId] = it }
|
null -> roomStore.retrieve(roomId)?.also { roomCache[roomId] = it }
|
||||||
else -> cached
|
else -> cached
|
||||||
@ -27,4 +29,9 @@ class RoomDataSource(
|
|||||||
roomStore.persist(roomId, newState)
|
roomStore.persist(roomId, newState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun remove(roomsLeft: List<RoomId>) {
|
||||||
|
roomsLeft.forEach { roomCache.remove(it) }
|
||||||
|
roomStore.remove(roomsLeft)
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,30 +4,35 @@ import app.dapk.st.core.CoroutineDispatchers
|
|||||||
import app.dapk.st.core.withIoContextAsync
|
import app.dapk.st.core.withIoContextAsync
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.*
|
||||||
import app.dapk.st.matrix.common.MatrixLogTag.SYNC
|
import app.dapk.st.matrix.common.MatrixLogTag.SYNC
|
||||||
|
import app.dapk.st.matrix.sync.InviteMeta
|
||||||
import app.dapk.st.matrix.sync.RoomInvite
|
import app.dapk.st.matrix.sync.RoomInvite
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
import app.dapk.st.matrix.sync.internal.request.ApiAccountEvent
|
import app.dapk.st.matrix.sync.internal.request.*
|
||||||
import app.dapk.st.matrix.sync.internal.request.ApiSyncResponse
|
|
||||||
import app.dapk.st.matrix.sync.internal.request.ApiSyncRoom
|
|
||||||
import app.dapk.st.matrix.sync.internal.room.SideEffectResult
|
import app.dapk.st.matrix.sync.internal.room.SideEffectResult
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
|
||||||
internal class SyncReducer(
|
internal class SyncReducer(
|
||||||
private val roomProcessor: RoomProcessor,
|
private val roomProcessor: RoomProcessor,
|
||||||
private val roomRefresher: RoomRefresher,
|
private val roomRefresher: RoomRefresher,
|
||||||
|
private val roomDataSource: RoomDataSource,
|
||||||
private val logger: MatrixLogger,
|
private val logger: MatrixLogger,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class ReducerResult(
|
data class ReducerResult(
|
||||||
|
val newRoomsJoined: List<RoomId>,
|
||||||
val roomState: List<RoomState>,
|
val roomState: List<RoomState>,
|
||||||
val invites: List<RoomInvite>
|
val invites: List<RoomInvite>,
|
||||||
|
val roomsLeft: List<RoomId>
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult {
|
suspend fun reduce(isInitialSync: Boolean, sideEffects: SideEffectResult, response: ApiSyncResponse, userCredentials: UserCredentials): ReducerResult {
|
||||||
val directMessages = response.directMessages()
|
val directMessages = response.directMessages()
|
||||||
|
|
||||||
val invites = response.rooms?.invite?.keys?.map { RoomInvite(it) } ?: emptyList()
|
val invites = response.rooms?.invite?.map { roomInvite(it, userCredentials) } ?: emptyList()
|
||||||
|
val roomsLeft = findRoomsLeft(response, userCredentials)
|
||||||
|
val newRooms = response.rooms?.join?.keys?.filterNot { roomDataSource.contains(it) } ?: emptyList()
|
||||||
|
|
||||||
val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges()
|
val apiUpdatedRooms = response.rooms?.join?.keepRoomsWithChanges()
|
||||||
val apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) ->
|
val apiRoomsToProcess = apiUpdatedRooms?.map { (roomId, apiRoom) ->
|
||||||
logger.matrixLog(SYNC, "reducing: $roomId")
|
logger.matrixLog(SYNC, "reducing: $roomId")
|
||||||
@ -50,7 +55,34 @@ internal class SyncReducer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ReducerResult((apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(), invites)
|
roomDataSource.remove(roomsLeft)
|
||||||
|
|
||||||
|
return ReducerResult(
|
||||||
|
newRooms,
|
||||||
|
(apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(),
|
||||||
|
invites,
|
||||||
|
roomsLeft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findRoomsLeft(response: ApiSyncResponse, userCredentials: UserCredentials) = response.rooms?.leave?.filter {
|
||||||
|
it.value.state.stateEvents.filterIsInstance<ApiTimelineEvent.RoomMember>().any {
|
||||||
|
it.content.membership.isLeave() && it.senderId == userCredentials.userId
|
||||||
|
}
|
||||||
|
}?.map { it.key } ?: emptyList()
|
||||||
|
|
||||||
|
private fun roomInvite(entry: Map.Entry<RoomId, ApiSyncRoomInvite>, userCredentials: UserCredentials): RoomInvite {
|
||||||
|
val memberEvents = entry.value.state.events.filterIsInstance<ApiStrippedEvent.RoomMember>()
|
||||||
|
val invitee = memberEvents.first { it.content.membership?.isInvite() ?: false }
|
||||||
|
val from = memberEvents.first { it.sender == invitee.sender }
|
||||||
|
return RoomInvite(
|
||||||
|
RoomMember(from.sender, from.content.displayName, from.content.avatarUrl?.convertMxUrToUrl(userCredentials.homeServer)?.let { AvatarUrl(it) }),
|
||||||
|
roomId = entry.key,
|
||||||
|
inviteMeta = when (invitee.content.isDirect) {
|
||||||
|
true -> InviteMeta.DirectMessage
|
||||||
|
null, false -> InviteMeta.Room(entry.value.state.events.filterIsInstance<ApiStrippedEvent.RoomName>().firstOrNull()?.content?.name)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,9 +46,15 @@ internal class SyncUseCase(
|
|||||||
val nextState = logger.logP("reducing") { syncReducer.reduce(isInitialSync, sideEffects, response, credentials) }
|
val nextState = logger.logP("reducing") { syncReducer.reduce(isInitialSync, sideEffects, response, credentials) }
|
||||||
val overview = nextState.roomState.map { it.roomOverview }
|
val overview = nextState.roomState.map { it.roomOverview }
|
||||||
|
|
||||||
|
if (nextState.roomsLeft.isNotEmpty()) {
|
||||||
|
persistence.removeRooms(nextState.roomsLeft)
|
||||||
|
}
|
||||||
if (nextState.invites.isNotEmpty()) {
|
if (nextState.invites.isNotEmpty()) {
|
||||||
persistence.persistInvites(nextState.invites)
|
persistence.persistInvites(nextState.invites)
|
||||||
}
|
}
|
||||||
|
if (nextState.newRoomsJoined.isNotEmpty()) {
|
||||||
|
persistence.removeInvites(nextState.newRoomsJoined)
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
previousState == overview -> previousState.also { logger.matrixLog(SYNC, "no changes, not persisting new state") }
|
previousState == overview -> previousState.also { logger.matrixLog(SYNC, "no changes, not persisting new state") }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user