diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index aff9bee..075037b 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -19,6 +19,7 @@ import app.dapk.st.notifications.NotificationsModule import app.dapk.st.notifications.PushAndroidService import app.dapk.st.profile.ProfileModule import app.dapk.st.settings.SettingsModule +import app.dapk.st.share.ShareEntryModule import app.dapk.st.work.TaskRunnerModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -75,6 +76,7 @@ class SmallTalkApplication : Application(), ModuleProvider { MessengerModule::class -> featureModules.messengerModule TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule CoreAndroidModule::class -> appModule.coreAndroidModule + ShareEntryModule::class -> featureModules.shareEntryModule else -> throw IllegalArgumentException("Unknown: $klass") } as T } diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 164d4a9..f5ea076 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -51,6 +51,7 @@ import app.dapk.st.olm.OlmWrapper import app.dapk.st.profile.ProfileModule import app.dapk.st.push.PushModule import app.dapk.st.settings.SettingsModule +import app.dapk.st.share.ShareEntryModule import app.dapk.st.tracking.TrackingModule import app.dapk.st.work.TaskRunnerModule import app.dapk.st.work.WorkModule @@ -178,6 +179,10 @@ internal class FeatureModules internal constructor( ) } + val shareEntryModule by unsafeLazy { + ShareEntryModule(matrixModules.sync, matrixModules.room) + } + } internal class MatrixModules( @@ -358,6 +363,7 @@ internal class MatrixModules( val roomService = services.roomService() object : RoomMembersService { override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds) + override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId) override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members) } }, diff --git a/core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt b/core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt new file mode 100644 index 0000000..eaf6813 --- /dev/null +++ b/core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt @@ -0,0 +1,4 @@ +package app.dapk.st.core + +@JvmInline +value class AndroidUri(val value: String) \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt index 101b39f..4b3b20e 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt @@ -35,4 +35,12 @@ class MemberPersistence( .map { Json.decodeFromString(RoomMember.serializer(), it) } } } + + override suspend fun query(roomId: RoomId, limit: Int): List { + return coroutineDispatchers.withIoContext { + database.roomMemberQueries.selectMembersByRoom(roomId.value, limit.toLong()) + .executeAsList() + .map { Json.decodeFromString(RoomMember.serializer(), it) } + } + } } \ No newline at end of file diff --git a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq index eff8ab1..081bccd 100644 --- a/domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq +++ b/domains/store/src/main/sqldelight/app/dapk/db/model/RoomMember.sq @@ -10,6 +10,13 @@ SELECT blob FROM dbRoomMember WHERE room_id = ? AND user_id IN ?; +selectMembersByRoom: +SELECT blob +FROM dbRoomMember +WHERE room_id = ? +LIMIT ?; + + insert: INSERT OR REPLACE INTO dbRoomMember(user_id, room_id, blob) VALUES (?, ?, ?); \ No newline at end of file diff --git a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt index cb3ae02..a73e2fa 100644 --- a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt +++ b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt @@ -29,6 +29,10 @@ interface Navigator { activity.navigateUpTo(intentFactory.home(activity)) } + fun toMessenger(roomId: RoomId) { + intentFactory.messenger(activity, roomId) + } + fun toFilePicker(requestCode: Int) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) diff --git a/features/share-entry/build.gradle b/features/share-entry/build.gradle index a8ead76..3e36d64 100644 --- a/features/share-entry/build.gradle +++ b/features/share-entry/build.gradle @@ -4,6 +4,9 @@ dependencies { implementation project(":domains:android:core") implementation project(":domains:android:viewmodel") implementation project(':domains:store') + implementation project(':matrix:services:sync') + implementation project(':matrix:services:room') + implementation project(':matrix:services:message') implementation project(":core") implementation project(":design-library") } \ No newline at end of file diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt index 672188e..b4651d5 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt @@ -4,14 +4,30 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.Parcelable +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.ui.Modifier import app.dapk.st.core.DapkActivity +import app.dapk.st.core.module +import app.dapk.st.core.viewModel +import app.dapk.st.design.components.SmallTalkTheme class ShareEntryActivity : DapkActivity() { + private val viewModel by viewModel { module().shareEntryViewModel() } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val urisToShare = intent.readSendUrisOrNull() ?: throw IllegalArgumentException("") - + setContent { + SmallTalkTheme { + Surface(Modifier.fillMaxSize()) { + ShareEntryScreen(viewModel) + } + } + } + viewModel.withUris(urisToShare) } } diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryModule.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryModule.kt new file mode 100644 index 0000000..ac0f61b --- /dev/null +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryModule.kt @@ -0,0 +1,15 @@ +package app.dapk.st.share + +import app.dapk.st.core.ProvidableModule +import app.dapk.st.matrix.room.RoomService +import app.dapk.st.matrix.sync.SyncService + +class ShareEntryModule( + private val syncService: SyncService, + private val roomService: RoomService, +) : ProvidableModule { + + fun shareEntryViewModel(): ShareEntryViewModel { + return ShareEntryViewModel(FetchRoomsUseCase(syncService, roomService)) + } +} \ No newline at end of file diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryScreen.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryScreen.kt new file mode 100644 index 0000000..634c6dd --- /dev/null +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryScreen.kt @@ -0,0 +1,125 @@ +package app.dapk.st.share + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dapk.st.core.LifecycleEffect +import app.dapk.st.core.StartObserving +import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.design.components.CircleishAvatar +import app.dapk.st.design.components.GenericEmpty +import app.dapk.st.design.components.GenericError +import app.dapk.st.design.components.Toolbar +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.share.DirectoryScreenState.* + +@Composable +fun ShareEntryScreen(viewModel: ShareEntryViewModel) { + val state = viewModel.state + + val listState: LazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = 0, + ) + + viewModel.ObserveEvents(listState) + + LifecycleEffect( + onStart = { viewModel.start() }, + onStop = { viewModel.stop() } + ) + + Box(modifier = Modifier.fillMaxSize()) { + Toolbar(title = "Send to...") + when (state) { + EmptyLoading -> CenteredLoading() + Empty -> GenericEmpty() + is Error -> GenericError { + // TODO + } + is Content -> Content(listState, state) + } + } +} + +@Composable +private fun ShareEntryViewModel.ObserveEvents(listState: LazyListState) { + val context = LocalContext.current + StartObserving { + this@ObserveEvents.events.launch { + when (it) { + is DirectoryEvent.SelectRoom -> TODO() + } + } + } +} + + +@Composable +private fun Content(listState: LazyListState, state: Content) { + val context = LocalContext.current + val navigateToRoom = { roomId: RoomId -> + // todo +// context.startActivity(MessengerActivity.newInstance(context, roomId)) + } + LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(top = 72.dp)) { + items( + items = state.items, + key = { it.id.value }, + ) { + DirectoryItem(it, onClick = navigateToRoom) + } + } +} + +@Composable +private fun DirectoryItem(item: Item, onClick: (RoomId) -> Unit) { + val roomName = item.roomName + + Box( + Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth() + .clickable { + onClick(item.id) + }) { + Row(Modifier.padding(20.dp)) { + val secondaryText = MaterialTheme.colors.onBackground.copy(alpha = 0.5f) + + Box(Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) { + CircleishAvatar(item.roomAvatarUrl?.value, roomName, size = 50.dp) + } + Spacer(Modifier.width(20.dp)) + Column { + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier.weight(1f), + maxLines = 1, + fontSize = 18.sp, + text = roomName, + overflow = TextOverflow.Ellipsis, + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.onBackground + ) + Spacer(modifier = Modifier.width(6.dp)) + } + Text(text = item.members.joinToString(), color = secondaryText, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + } + } +} + diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryState.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryState.kt new file mode 100644 index 0000000..17a4737 --- /dev/null +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryState.kt @@ -0,0 +1,19 @@ +package app.dapk.st.share + +import app.dapk.st.matrix.common.AvatarUrl +import app.dapk.st.matrix.common.RoomId + +sealed interface DirectoryScreenState { + + object EmptyLoading : DirectoryScreenState + object Empty : DirectoryScreenState + data class Content( + val items: List, + ) : DirectoryScreenState +} + +sealed interface DirectoryEvent { + data class SelectRoom(val item: Item) : DirectoryEvent +} + +data class Item(val id: RoomId, val roomAvatarUrl: AvatarUrl?, val roomName: String, val members: List) diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt new file mode 100644 index 0000000..db3e9cc --- /dev/null +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt @@ -0,0 +1,64 @@ +package app.dapk.st.share + +import android.net.Uri +import androidx.lifecycle.viewModelScope +import app.dapk.st.core.AndroidUri +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.message.MessageService +import app.dapk.st.matrix.room.RoomService +import app.dapk.st.matrix.sync.SyncService +import app.dapk.st.viewmodel.DapkViewModel +import app.dapk.st.viewmodel.MutableStateFactory +import app.dapk.st.viewmodel.defaultStateFactory +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class ShareEntryViewModel( + private val fetchRoomsUseCase: FetchRoomsUseCase, + factory: MutableStateFactory = defaultStateFactory(), +) : DapkViewModel( + initialState = DirectoryScreenState.EmptyLoading, + factory, +) { + + private var syncJob: Job? = null + + fun start() { + syncJob = viewModelScope.launch { + state = DirectoryScreenState.Content(fetchRoomsUseCase.bar()) + } + } + + fun stop() { + syncJob?.cancel() + } + + + fun sendAttachment() { + + } + + fun withUris(urisToShare: List) { +// TODO("Not yet implemented") + } + +} + +class FetchRoomsUseCase( + private val syncSyncService: SyncService, + private val roomService: RoomService, +) { + + suspend fun bar(): List { + return syncSyncService.overview().first().map { + Item( + it.roomId, + it.roomAvatarUrl, + it.roomName ?: "", + roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value } + ) + } + } +} + diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt index 41f36d1..92bc1d0 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt @@ -21,6 +21,7 @@ interface RoomService : MatrixService { suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? suspend fun findMembers(roomId: RoomId, userIds: List): List + suspend fun findMembersSummary(roomId: RoomId): List suspend fun insertMembers(roomId: RoomId, members: List) suspend fun createDm(userId: UserId, encrypted: Boolean): RoomId @@ -50,6 +51,7 @@ fun MatrixServiceProvider.roomService(): RoomService = this.getService(key = SER interface MemberStore { suspend fun insert(roomId: RoomId, members: List) suspend fun query(roomId: RoomId, userIds: List): List + suspend fun query(roomId: RoomId, limit: Int): List } interface RoomMessenger { diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt index aa33c60..4ad8ef5 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt @@ -39,6 +39,10 @@ class DefaultRoomService( return roomMembers.findMembers(roomId, userIds) } + override suspend fun findMembersSummary(roomId: RoomId): List { + return roomMembers.findMembersSummary(roomId) + } + override suspend fun insertMembers(roomId: RoomId, members: List) { roomMembers.insert(roomId, members) } diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt index c3a9aff..9c7a03b 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/RoomMembers.kt @@ -36,6 +36,8 @@ class RoomMembers(private val memberStore: MemberStore, private val membersCache } } + suspend fun findMembersSummary(roomId: RoomId) = memberStore.query(roomId, limit = 8) + suspend fun insert(roomId: RoomId, members: List) { membersCache.insert(roomId, members) memberStore.insert(roomId, members) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt index 7fc345c..f0c8530 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt @@ -128,6 +128,7 @@ internal object NoOpKeySharer : KeySharer { interface RoomMembersService { suspend fun find(roomId: RoomId, userIds: List): List + suspend fun findSummary(roomId: RoomId): List suspend fun insert(roomId: RoomId, members: List) } diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index 9f775b3..c324929 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -246,6 +246,7 @@ class TestMatrix( val roomService = services.roomService() object : RoomMembersService { override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds) + override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId) override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members) } },