Basic peeking preview and join and filter

This commit is contained in:
Valere 2021-01-07 10:43:00 +01:00
parent c5fa0a413f
commit df341d8ea3
42 changed files with 1604 additions and 31 deletions

View file

@ -52,6 +52,7 @@ object EventType {
const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
const val STATE_SPACE_CHILD = "m.space.child"
// const val STATE_SPACE_CHILD = "org.matrix.msc1772.space"
/**
* Note that this Event has been deprecated, see

View file

@ -34,4 +34,6 @@ sealed class PeekResult {
) : PeekResult()
object UnknownAlias : PeekResult()
fun isSuccess() = this is Success
}

View file

@ -23,4 +23,6 @@ interface Space {
fun asRoom() : Room
suspend fun addRoom(roomId: String)
// fun getChildren() : List<IRoomSummary>
}

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.space
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
@ -35,6 +36,13 @@ interface SpaceService {
*/
fun getSpace(spaceId: String): Space?
/**
* Try to resolve (peek) rooms and subspace in this space.
* Use this call get preview of children of this space, particularly useful to get a
* preview of rooms that you did not join yet.
*/
suspend fun peekSpace(spaceId: String) : SpacePeekResult
/**
* Get a live list of space summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of List[SpaceSummary]
@ -42,4 +50,23 @@ interface SpaceService {
fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<SpaceSummary>>
fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<SpaceSummary>
data class ChildAutoJoinInfo(
val roomIdOrAlias: String,
val viaServers: List<String> = emptyList()
)
sealed class JoinSpaceResult {
object Success: JoinSpaceResult()
data class Fail(val error: Throwable?): JoinSpaceResult()
/** Success fully joined the space, but failed to join all or some of it's rooms */
data class PartialSuccess(val failedRooms: Map<String, Throwable>) : JoinSpaceResult()
fun isSuccess() = this is Success || this is PartialSuccess
}
suspend fun joinSpace(spaceIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList(),
autoJoinChild: List<ChildAutoJoinInfo>) : JoinSpaceResult
}

View file

@ -35,6 +35,7 @@ data class SpaceChildContent(
@Json(name = "via") val via: List<String>? = null,
/**
* present: true key is included to distinguish from a deleted state event
* Children where present is not present or is not set to true are ignored.
*/
@Json(name = "present") val present: Boolean? = false,
/**

View file

@ -91,6 +91,8 @@ import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask
import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask
import org.matrix.android.sdk.internal.session.space.DefaultSpaceService
import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
import retrofit2.Retrofit
@Module
@ -236,6 +238,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask
@Binds
abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask
@Binds
abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask
}

View file

@ -44,7 +44,7 @@ internal class RoomRelationshipHelper(private val realm: Realm,
.filter { ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.present == true }
.mapNotNull {
// ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()
it.roomId
it.stateKey
}
}
}

View file

@ -167,7 +167,7 @@ internal class RoomSummaryUpdater @Inject constructor(
spaceSummaryEntity.children.clear()
spaceSummaryEntity.children.addAll(
RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions()
.map { RoomSummaryEntity.getOrCreate(realm, roomId) }
.map { RoomSummaryEntity.getOrCreate(realm, it) }
)
}
}

View file

@ -35,4 +35,12 @@ class DefaultSpace(private val room: Room) : Space {
body = SpaceChildContent(present = true).toContent()
)
}
// override fun getChildren(): List<IRoomSummary> {
// // asRoom().getStateEvents(setOf(EventType.STATE_SPACE_CHILD)).mapNotNull {
// // // statekeys are the roomIds
// //
// // }
// return emptyList<IRoomSummary>()
// }
}

View file

@ -32,6 +32,8 @@ import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject
@ -46,6 +48,7 @@ internal class DefaultSpaceService @Inject constructor(
private val deleteRoomAliasTask: DeleteRoomAliasTask,
private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource,
private val peekSpaceTask: PeekSpaceTask,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor
) : SpaceService {
@ -67,4 +70,31 @@ internal class DefaultSpaceService @Inject constructor(
override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<SpaceSummary> {
return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
}
override suspend fun peekSpace(spaceId: String): SpacePeekResult {
return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId))
}
override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, viaServers: List<String>, autoJoinChild: List<SpaceService.ChildAutoJoinInfo>): SpaceService.JoinSpaceResult {
try {
joinRoomTask.execute(JoinRoomTask.Params(spaceIdOrAlias, reason, viaServers))
val childJoinFailures = mutableMapOf<String, Throwable>()
autoJoinChild.forEach { info ->
// TODO what if the child is it self a subspace with some default children?
try {
joinRoomTask.execute(JoinRoomTask.Params(info.roomIdOrAlias, null, info.viaServers))
} catch (failure: Throwable) {
// TODO, i could already be a member of this room, handle that as it should not be an error in this context
childJoinFailures[info.roomIdOrAlias] = failure
}
}
return if (childJoinFailures.isEmpty()) {
SpaceService.JoinSpaceResult.Success
} else {
SpaceService.JoinSpaceResult.PartialSuccess(childJoinFailures)
}
} catch (throwable: Throwable) {
return SpaceService.JoinSpaceResult.Fail(throwable)
}
}
}

View file

@ -0,0 +1,141 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.space.peeking
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject
internal interface PeekSpaceTask : Task<PeekSpaceTask.Params, SpacePeekResult> {
data class Params(
val roomIdOrAlias: String,
// A depth limit as a simple protection against cycles
val maxDepth: Int = 4
)
}
internal class DefaultPeekSpaceTask @Inject constructor(
private val peekRoomTask: PeekRoomTask,
private val resolveRoomStateTask: ResolveRoomStateTask
) : PeekSpaceTask {
override suspend fun execute(params: PeekSpaceTask.Params): SpacePeekResult {
val peekResult = peekRoomTask.execute(PeekRoomTask.Params(params.roomIdOrAlias))
val roomResult = peekResult as? PeekResult.Success ?: return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult)
// check the room type
// kind of duplicate cause we already did it in Peek? could we pass on the result??
val stateEvents = try {
resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomResult.roomId))
} catch (failure: Throwable) {
return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult)
}
val isSpace = stateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" }
?.let { it.content?.toModel<RoomCreateContent>()?.type } == RoomType.SPACE
if (!isSpace) return SpacePeekResult.NotSpaceType(params.roomIdOrAlias)
val children = peekChildren(stateEvents, 0, params.maxDepth)
return SpacePeekResult.Success(
SpacePeekSummary(
params.roomIdOrAlias,
peekResult,
children
)
)
}
private suspend fun peekChildren(stateEvents: List<Event>, depth: Int, maxDepth: Int): List<ISpaceChild> {
if (depth >= maxDepth) return emptyList()
val childRoomsIds = stateEvents
.filter {
it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty()
// Children where present is not present or is not set to true are ignored.
&& it.content?.toModel<SpaceChildContent>()?.present == true
}
.map { it.stateKey to it.content?.toModel<SpaceChildContent>() }
Timber.v("## SPACE_PEEK: found ${childRoomsIds.size} present children")
val spaceChildResults = mutableListOf<ISpaceChild>()
childRoomsIds.forEach { entry ->
Timber.v("## SPACE_PEEK: peeking child $entry")
// peek each child
val childId = entry.first ?: return@forEach
try {
val childPeek = peekRoomTask.execute(PeekRoomTask.Params(childId))
val childStateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(childId))
val createContent = childStateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" }
?.let { it.content?.toModel<RoomCreateContent>() }
if (!childPeek.isSuccess() || createContent == null) {
Timber.v("## SPACE_PEEK: cannot peek child $entry")
// can't peek :/
spaceChildResults.add(
SpaceChildPeekResult(
childId, childPeek, entry.second?.default, entry.second?.order
)
)
// continue to next child
return@forEach
}
val type = createContent.type
if (type == RoomType.SPACE) {
Timber.v("## SPACE_PEEK: subspace child $entry")
spaceChildResults.add(
SpaceSubChildPeekResult(
childId,
childPeek,
entry.second?.default,
entry.second?.order,
peekChildren(childStateEvents, depth + 1, maxDepth)
)
)
} else if (type == RoomType.MESSAGING || type == null) {
Timber.v("## SPACE_PEEK: room child $entry")
spaceChildResults.add(
SpaceChildPeekResult(
childId, childPeek, entry.second?.default, entry.second?.order
)
)
} else {
// ignore for now?
}
// let's check child info
} catch (failure: Throwable) {
// can this happen?
Timber.e(failure, "## Failed to resolve space child")
}
}
return spaceChildResults
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.space.peeking
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
data class SpacePeekSummary(
val idOrAlias: String,
val roomPeekResult: PeekResult.Success,
val children: List<ISpaceChild>
)
interface ISpaceChild {
val id: String
val roomPeekResult: PeekResult
val default: Boolean?
val order: String?
}
data class SpaceChildPeekResult(
override val id: String,
override val roomPeekResult: PeekResult,
override val default: Boolean? = null,
override val order: String? = null
) : ISpaceChild
data class SpaceSubChildPeekResult(
override val id: String,
override val roomPeekResult: PeekResult,
override val default: Boolean?,
override val order: String?,
val children: List<ISpaceChild>
) : ISpaceChild
sealed class SpacePeekResult {
abstract class SpacePeekError : SpacePeekResult()
data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError()
data class NotSpaceType(val spaceId: String) : SpacePeekError()
data class Success(val summary: SpacePeekSummary): SpacePeekResult()
}

View file

@ -271,7 +271,8 @@
<!-- </intent-filter>-->
</activity>
<activity android:name=".features.devtools.RoomDevToolActivity" />
<activity android:name=".features.devtools.RoomDevToolActivity"/>
<activity android:name=".features.spaces.SpacePreviewActivity"/>
<!-- Services -->
<service

View file

@ -118,6 +118,7 @@ import im.vector.app.features.settings.push.PushRulesFragment
import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
import im.vector.app.features.share.IncomingShareFragment
import im.vector.app.features.signout.soft.SoftLogoutFragment
import im.vector.app.features.spaces.preview.SpacePreviewFragment
import im.vector.app.features.terms.ReviewTermsFragment
import im.vector.app.features.usercode.ShowUserCodeFragment
import im.vector.app.features.userdirectory.UserListFragment
@ -630,4 +631,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(RoomDevToolSendFormFragment::class)
fun bindRoomDevToolSendFormFragment(fragment: RoomDevToolSendFormFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SpacePreviewFragment::class)
fun bindSpacePreviewFragment(fragment: SpacePreviewFragment): Fragment
}

View file

@ -38,6 +38,7 @@ import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel
import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel
import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
@Module
@ -142,4 +143,9 @@ interface ViewModelModule {
@IntoMap
@ViewModelKey(DiscoverySharedViewModel::class)
fun bindDiscoverySharedViewModel(viewModel: DiscoverySharedViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(SpacePreviewSharedActionViewModel::class)
fun bindSpacePreviewSharedActionViewModel(viewModel: SpacePreviewSharedActionViewModel): ViewModel
}

View file

@ -47,7 +47,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session),
CONFETTI("/confetti", "<message>", R.string.command_confetti),
SNOW("/snow", "<message>", R.string.command_snow),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space);
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space),
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space);
val length
get() = command.length + 1

View file

@ -296,7 +296,7 @@ object CommandParser {
val message = textMessage.substring(Command.CONFETTI.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message)
}
Command.SNOW.command -> {
Command.SNOW.command -> {
val message = textMessage.substring(Command.SNOW.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.SNOW, message)
}
@ -312,6 +312,12 @@ object CommandParser {
)
}
}
Command.ADD_TO_SPACE.command -> {
val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim()
ParsedCommand.AddToSpace(
rawCommand
)
}
else -> {
// Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View file

@ -58,4 +58,5 @@ sealed class ParsedCommand {
object DiscardSession : ParsedCommand()
class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand()
class CreateSpace(val name: String, val invitees: List<String>) : ParsedCommand()
class AddToSpace(val spaceId: String) : ParsedCommand()
}

View file

@ -59,7 +59,8 @@ class SpaceListFragment @Inject constructor(
views.groupListView.configureWith(spaceController)
viewModel.observeViewEvents {
when (it) {
is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id))
is SpaceListViewEvents.OpenSpace -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
}.exhaustive
}
}

View file

@ -18,9 +18,13 @@
package im.vector.app.features.grouplist
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericItemHeader
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.spaces.SpaceListViewState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -50,24 +54,57 @@ class SpaceSummaryController @Inject constructor(
if (summaries.isNullOrEmpty()) {
return
}
summaries.forEach { groupSummary ->
val isSelected = groupSummary.spaceId == selected?.spaceId
if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) {
homeSpaceSummaryItem {
id(groupSummary.spaceId)
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
// show invites on top
summaries.filter { it.roomSummary.membership == Membership.INVITE }
.let { invites ->
if (invites.isNotEmpty()) {
genericItemHeader {
id("invites")
text(stringProvider.getString(R.string.spaces_invited_header))
}
invites.forEach {
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(it.spaceId)
matrixItem(it.toMatrixItem())
selected(false)
listener { callback?.onSpaceSelected(it) }
}
}
genericFooterItem {
id("invite_space")
text("")
}
}
}
} else {
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.spaceId)
matrixItem(groupSummary.toMatrixItem())
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
}
}
genericItemHeader {
id("spaces")
text(stringProvider.getString(R.string.spaces_header))
}
summaries
.filter { it.roomSummary.membership == Membership.JOIN }
.forEach { groupSummary ->
val isSelected = groupSummary.spaceId == selected?.spaceId
if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) {
homeSpaceSummaryItem {
id(groupSummary.spaceId)
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
}
} else {
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.spaceId)
matrixItem(groupSummary.toMatrixItem())
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
}
}
}
}
interface Callback {

View file

@ -54,6 +54,7 @@ import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.spaces.SpacePreviewActivity
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState
@ -141,6 +142,9 @@ class HomeActivity :
views.drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
}
is HomeActivitySharedAction.OpenSpacePreview -> {
startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
}
}.exhaustive
}
.disposeOnDestroy()

View file

@ -25,4 +25,5 @@ sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction()
object CloseDrawer : HomeActivitySharedAction()
object OpenGroup : HomeActivitySharedAction()
data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction()
}

View file

@ -839,6 +839,17 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.AddToSpace -> {
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().getSpace(slashCommandResult.spaceId)?.addRoom(room.roomId)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
}
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
}.exhaustive
}
is SendMode.EDIT -> {

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.airbnb.mvrx.MvRx
import im.vector.app.R
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.spaces.preview.SpacePreviewArgs
import im.vector.app.features.spaces.preview.SpacePreviewFragment
class SpacePreviewActivity : VectorBaseActivity<ActivitySimpleBinding>() {
lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel
override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedActionViewModel = viewModelProvider.get(SpacePreviewSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { action ->
when (action) {
SpacePreviewSharedAction.DismissAction -> finish()
SpacePreviewSharedAction.ShowModalLoading -> showWaitingView()
SpacePreviewSharedAction.HideModalLoading -> hideWaitingView()
is SpacePreviewSharedAction.ShowErrorMessage -> action.error?.let { showSnackbar(it) }
}
}.disposeOnDestroy()
if (isFirstCreation()) {
val simpleName = SpacePreviewFragment::class.java.simpleName
val args = intent?.getParcelableExtra<SpacePreviewArgs>(MvRx.KEY_ARG)
if (supportFragmentManager.findFragmentByTag(simpleName) == null) {
supportFragmentManager.commitTransaction {
replace(R.id.simpleFragmentContainer,
SpacePreviewFragment::class.java,
Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) },
simpleName
)
}
}
}
}
companion object {
fun newIntent(context: Context, spaceIdOrAlias: String): Intent {
return Intent(context, SpacePreviewActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, SpacePreviewArgs(spaceIdOrAlias))
}
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces
import im.vector.app.core.platform.VectorSharedAction
import im.vector.app.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
sealed class SpacePreviewSharedAction : VectorSharedAction {
object DismissAction : SpacePreviewSharedAction()
object ShowModalLoading : SpacePreviewSharedAction()
object HideModalLoading : SpacePreviewSharedAction()
data class ShowErrorMessage(val error: String? = null) : SpacePreviewSharedAction()
}
class SpacePreviewSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<SpacePreviewSharedAction>()

View file

@ -53,7 +53,8 @@ sealed class SpaceListAction : VectorViewModelAction {
* Transient events for group list screen
*/
sealed class SpaceListViewEvents : VectorViewEvents {
object OpenSpaceSummary : SpaceListViewEvents()
object OpenSpace : SpaceListViewEvents()
data class OpenSpaceSummary(val id: String) : SpaceListViewEvents()
}
data class SpaceListViewState(
@ -94,7 +95,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
// We only want to open group if the updated selectedGroup is a different one.
if (currentGroupId != spaceSummary.spaceId) {
currentGroupId = spaceSummary.spaceId
_viewEvents.post(SpaceListViewEvents.OpenSpaceSummary)
_viewEvents.post(SpaceListViewEvents.OpenSpace)
}
val optionGroup = Option.just(spaceSummary)
selectedSpaceDataSource.post(optionGroup)
@ -116,20 +117,27 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
// PRIVATE METHODS *****************************************************************************
private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state ->
if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) {
// We take care of refreshing group data when selecting to be sure we get all the rooms and users
// tryOrNull {
// viewModelScope.launch {
// session.getGroup(action.spaceSummary.groupId)?.fetchGroupData()
if (state.selectedSpace?.roomSummary?.membership == Membership.INVITE) {
_viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(state.selectedSpace.roomSummary.roomId))
// viewModelScope.launch(Dispatchers.IO) {
// tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let {
// Timber.d("PEEK RESULT/ $it")
// }
// }
setState { copy(selectedSpace = action.spaceSummary) }
} else {
if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) {
// state.selectedSpace?.let {
// selectedSpaceDataSource.post(Option.just(state.selectedSpace))
// }
setState { copy(selectedSpace = action.spaceSummary) }
}
}
}
private fun observeGroupSummaries() {
val roomSummaryQueryParams = roomSummaryQueryParams() {
memberships = listOf(Membership.JOIN)
memberships = listOf(Membership.JOIN, Membership.INVITE)
displayName = QueryStringValue.IsNotEmpty
excludeType = listOf(RoomType.MESSAGING, null)
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_space_roomchild)
abstract class RoomChildItem : VectorEpoxyModel<RoomChildItem.Holder>() {
@EpoxyAttribute
lateinit var roomId: String
@EpoxyAttribute
lateinit var title: String
@EpoxyAttribute
var topic: String? = null
@EpoxyAttribute
lateinit var memberCount: String
@EpoxyAttribute
var avatarUrl: String? = null
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
var depth: Int = 0
override fun bind(holder: Holder) {
super.bind(holder)
holder.roomNameText.text = title
holder.roomTopicText.setTextOrHide(topic)
holder.memberCountText.text = memberCount
avatarRenderer.render(
MatrixItem.RoomItem(roomId, title, avatarUrl),
holder.avatarImageView
)
holder.tabView.tabDepth = depth
}
override fun unbind(holder: Holder) {
avatarRenderer.clear(holder.avatarImageView)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val avatarImageView by bind<ImageView>(R.id.childRoomAvatar)
val roomNameText by bind<TextView>(R.id.childRoomName)
val roomTopicText by bind<TextView>(R.id.childRoomTopic)
val memberCountText by bind<TextView>(R.id.spaceChildMemberCountText)
val tabView by bind<SpaceTabView>(R.id.spaceChildTabView)
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericItemHeader
import im.vector.app.core.utils.TextUtils
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.internal.session.space.peeking.ISpaceChild
import org.matrix.android.sdk.internal.session.space.peeking.SpaceChildPeekResult
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
import org.matrix.android.sdk.internal.session.space.peeking.SpaceSubChildPeekResult
import javax.inject.Inject
class SpacePreviewController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider
) : TypedEpoxyController<SpacePreviewState>() {
interface InteractionListener
var interactionListener: InteractionListener? = null
override fun buildModels(data: SpacePreviewState?) {
val result: SpacePeekResult = data?.peekResult?.invoke() ?: return
when (result) {
is SpacePeekResult.SpacePeekError -> {
genericFooterItem {
id("failed")
// TODO
text("Failed to resolve")
}
}
is SpacePeekResult.Success -> {
// add summary info
val memberCount = result.summary.roomPeekResult.numJoinedMembers ?: 0
spaceTopSummaryItem {
id("info")
formattedMemberCount(stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount))
topic(result.summary.roomPeekResult.topic ?: "")
}
genericItemHeader {
id("header_rooms")
text(stringProvider.getString(R.string.rooms))
}
buildChildren(result.summary.children, 0)
}
}
}
private fun buildChildren(children: List<ISpaceChild>, depth: Int) {
children.forEach { child ->
when (child) {
is SpaceSubChildPeekResult -> {
when (val roomPeekResult = child.roomPeekResult) {
is PeekResult.Success -> {
subSpaceItem {
id(roomPeekResult.roomId)
roomId(roomPeekResult.roomId)
title(roomPeekResult.name)
depth(depth)
avatarUrl(roomPeekResult.avatarUrl)
avatarRenderer(avatarRenderer)
}
buildChildren(child.children, depth + 1)
}
else -> {
// ?? TODO
}
}
}
is SpaceChildPeekResult -> {
// We have to check if the peek result was success
when (val roomPeekResult = child.roomPeekResult) {
is PeekResult.Success -> {
roomChildItem {
id(child.id)
depth(depth)
roomId(roomPeekResult.roomId)
title(roomPeekResult.name ?: "")
topic(roomPeekResult.topic ?: "")
avatarUrl(roomPeekResult.avatarUrl)
memberCount(TextUtils.formatCountToShortDecimal(roomPeekResult.numJoinedMembers ?: 0))
avatarRenderer(avatarRenderer)
}
}
else -> {
// What to do here?
}
}
}
}
}
}
}

View file

@ -0,0 +1,165 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.appcompat.navigationClicks
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentSpacePreviewBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.spaces.SpacePreviewSharedAction
import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@Parcelize
data class SpacePreviewArgs(
val idOrAlias: String
) : Parcelable
class SpacePreviewFragment @Inject constructor(
private val viewModelFactory: SpacePreviewViewModel.Factory,
private val avatarRenderer: AvatarRenderer,
private val epoxyController: SpacePreviewController
) : VectorBaseFragment<FragmentSpacePreviewBinding>(), SpacePreviewViewModel.Factory {
private val viewModel by fragmentViewModel(SpacePreviewViewModel::class)
lateinit var sharedActionViewModel: SpacePreviewSharedActionViewModel
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSpacePreviewBinding {
return FragmentSpacePreviewBinding.inflate(inflater, container, false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(SpacePreviewSharedActionViewModel::class.java)
}
override fun create(initialState: SpacePreviewState) = viewModelFactory.create(initialState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.observeViewEvents {
handleViewEvents(it)
}
views.roomPreviewNoPreviewToolbar.navigationClicks()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction) }
.disposeOnDestroyView()
views.spacePreviewRecyclerView.configureWith(epoxyController)
views.spacePreviewAcceptInviteButton.debouncedClicks {
viewModel.handle(SpacePreviewViewAction.AcceptInvite)
}
views.spacePreviewDeclineInviteButton.debouncedClicks {
viewModel.handle(SpacePreviewViewAction.DismissInvite)
}
}
override fun onDestroyView() {
views.spacePreviewRecyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) {
when (it.peekResult) {
is Uninitialized,
is Loading -> {
views.spacePreviewPeekingProgress.isVisible = true
views.spacePreviewButtonBar.isVisible = true
views.spacePreviewAcceptInviteButton.isEnabled = false
views.spacePreviewDeclineInviteButton.isEnabled = false
}
is Fail -> {
views.spacePreviewPeekingProgress.isVisible = false
views.spacePreviewButtonBar.isVisible = false
}
is Success -> {
views.spacePreviewPeekingProgress.isVisible = false
views.spacePreviewButtonBar.isVisible = true
views.spacePreviewAcceptInviteButton.isEnabled = true
views.spacePreviewDeclineInviteButton.isEnabled = true
epoxyController.setData(it)
}
}
updateToolbar(it)
}
private fun handleViewEvents(viewEvents: SpacePreviewViewEvents) {
when (viewEvents) {
SpacePreviewViewEvents.Dismiss -> {
}
SpacePreviewViewEvents.StartJoining -> {
sharedActionViewModel.post(SpacePreviewSharedAction.ShowModalLoading)
}
SpacePreviewViewEvents.JoinSuccess -> {
sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading)
sharedActionViewModel.post(SpacePreviewSharedAction.DismissAction)
}
is SpacePreviewViewEvents.JoinFailure -> {
sharedActionViewModel.post(SpacePreviewSharedAction.HideModalLoading)
sharedActionViewModel.post(SpacePreviewSharedAction.ShowErrorMessage(viewEvents.message ?: getString(R.string.matrix_error)))
}
}
}
private fun updateToolbar(spacePreviewState: SpacePreviewState) {
when (val preview = spacePreviewState.peekResult.invoke()) {
is SpacePeekResult.Success -> {
val roomPeekResult = preview.summary.roomPeekResult
val mxItem = MatrixItem.RoomItem(roomPeekResult.roomId, roomPeekResult.name, roomPeekResult.avatarUrl)
avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar)
views.roomPreviewNoPreviewToolbarTitle.text = roomPeekResult.name
}
is SpacePeekResult.SpacePeekError,
null -> {
// what to do here?
val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spacePreviewState.name, spacePreviewState.avatarUrl)
avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar)
views.roomPreviewNoPreviewToolbarTitle.text = spacePreviewState.name
}
}
}
override fun onStart() {
super.onStart()
viewModel.handle(SpacePreviewViewAction.ViewReady)
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
data class SpacePreviewState(
val idOrAlias: String,
val name: String? = null,
val avatarUrl: String? = null,
val peekResult: Async<SpacePeekResult> = Uninitialized
) : MvRxState {
constructor(args: SpacePreviewArgs) : this(idOrAlias = args.idOrAlias)
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import im.vector.app.core.platform.VectorViewModelAction
sealed class SpacePreviewViewAction : VectorViewModelAction {
object ViewReady : SpacePreviewViewAction()
object AcceptInvite : SpacePreviewViewAction()
object DismissInvite : SpacePreviewViewAction()
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import im.vector.app.core.platform.VectorViewEvents
sealed class SpacePreviewViewEvents : VectorViewEvents {
object Dismiss: SpacePreviewViewEvents()
object StartJoining: SpacePreviewViewEvents()
object JoinSuccess: SpacePreviewViewEvents()
data class JoinFailure(val message: String?): SpacePreviewViewEvents()
}

View file

@ -0,0 +1,135 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
class SpacePreviewViewModel @AssistedInject constructor(
@Assisted private val initialState: SpacePreviewState,
private val session: Session
) : VectorViewModel<SpacePreviewState, SpacePreviewViewAction, SpacePreviewViewEvents>(initialState) {
private var initialized = false
init {
// do we have some things in cache?
session.getRoomSummary(initialState.idOrAlias)?.let {
setState {
copy(name = it.name, avatarUrl = it.avatarUrl)
}
}
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: SpacePreviewState): SpacePreviewViewModel
}
companion object : MvRxViewModelFactory<SpacePreviewViewModel, SpacePreviewState> {
override fun create(viewModelContext: ViewModelContext, state: SpacePreviewState): SpacePreviewViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
override fun handle(action: SpacePreviewViewAction) {
when (action) {
SpacePreviewViewAction.ViewReady -> handleReady()
SpacePreviewViewAction.AcceptInvite -> handleAcceptInvite()
SpacePreviewViewAction.DismissInvite -> handleDismissInvite()
}
}
private fun handleDismissInvite() {
TODO("Not yet implemented")
}
private fun handleAcceptInvite() = withState { state ->
// Here we need to join the space himself as well as the default rooms in that space
val spaceInfo = state.peekResult.invoke() as? SpacePeekResult.Success
// TODO if we have no summary, we cannot find auto join rooms...
// So maybe we should trigger a retry on summary after the join?
val spaceVia = (spaceInfo?.summary?.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList()
val autoJoinChildren = spaceInfo?.summary?.children
?.filter { it.default == true }
?.map {
SpaceService.ChildAutoJoinInfo(
it.id,
// via servers
(it.roomPeekResult as? PeekResult.Success)?.viaServers ?: emptyList()
)
} ?: emptyList()
// trigger modal loading
_viewEvents.post(SpacePreviewViewEvents.StartJoining)
viewModelScope.launch(Dispatchers.IO) {
val joinResult = session.spaceService().joinSpace(spaceInfo?.summary?.idOrAlias ?: initialState.idOrAlias, null, spaceVia, autoJoinChildren)
when (joinResult) {
SpaceService.JoinSpaceResult.Success,
is SpaceService.JoinSpaceResult.PartialSuccess -> {
// For now we don't handle partial success, it's just success
_viewEvents.post(SpacePreviewViewEvents.JoinSuccess)
}
is SpaceService.JoinSpaceResult.Fail -> {
_viewEvents.post(SpacePreviewViewEvents.JoinFailure(joinResult.error?.toString()))
}
}
}
}
private fun handleReady() {
if (!initialized) {
initialized = true
// peek for the room
setState {
copy(peekResult = Loading())
}
viewModelScope.launch(Dispatchers.IO) {
try {
val result = session.spaceService().peekSpace(initialState.idOrAlias)
setState {
copy(peekResult = Success(result))
}
} catch (failure: Throwable) {
setState {
copy(peekResult = Fail(failure))
}
}
}
}
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import im.vector.app.R
class SpaceTabView constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr) {
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {}
constructor(context: Context) : this(context, null, 0) {}
var tabDepth = 0
set(value) {
if (field != value) {
field = value
setUpView()
}
}
init {
setUpView()
}
private fun setUpView() {
// remove children
removeAllViews()
for (i in 0 until tabDepth) {
inflate(context, R.layout.item_space_tab, this)
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_space_top_summary)
abstract class SpaceTopSummaryItem : VectorEpoxyModel<SpaceTopSummaryItem.Holder>() {
@EpoxyAttribute
var topic: String? = null
@EpoxyAttribute
lateinit var formattedMemberCount: String
override fun bind(holder: Holder) {
super.bind(holder)
holder.spaceTopicText.setTextOrHide(topic)
holder.memberCountText.text = formattedMemberCount
}
class Holder : VectorEpoxyHolder() {
val memberCountText by bind<TextView>(R.id.spaceSummaryMemberCountText)
val spaceTopicText by bind<TextView>(R.id.spaceSummaryTopic)
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.preview
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_space_subspace)
abstract class SubSpaceItem : VectorEpoxyModel<SubSpaceItem.Holder>() {
@EpoxyAttribute
lateinit var roomId: String
@EpoxyAttribute
lateinit var title: String
@EpoxyAttribute
var avatarUrl: String? = null
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
var depth: Int = 0
override fun bind(holder: Holder) {
super.bind(holder)
holder.nameText.text = title
avatarRenderer.renderSpace(
MatrixItem.RoomItem(roomId, title, avatarUrl),
holder.avatarImageView
)
holder.tabView.tabDepth = depth
}
override fun unbind(holder: Holder) {
avatarRenderer.clear(holder.avatarImageView)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val avatarImageView by bind<ImageView>(R.id.childSpaceAvatar)
val nameText by bind<TextView>(R.id.childSpaceName)
val tabView by bind<SpaceTabView>(R.id.childSpaceTab)
}
}

View file

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomPreviewNoPreviewToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:navigationIcon="@drawable/ic_x_18dp"
android:elevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- <ImageView-->
<!-- android:id="@+id/spacePreviewClose"-->
<!-- android:layout_width="@dimen/layout_touch_size"-->
<!-- android:layout_height="@dimen/layout_touch_size"-->
<!-- android:clickable="true"-->
<!-- android:focusable="true"-->
<!-- android:foreground="?attr/selectableItemBackground"-->
<!-- android:scaleType="center"-->
<!-- android:src="@drawable/ic_x_18dp"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent" />-->
<ImageView
android:id="@+id/spacePreviewToolbarAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/roomPreviewNoPreviewToolbarTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_toolbar_primary_text_color"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/spacePreviewToolbarAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<ProgressBar
android:id="@+id/spacePreviewPeekingProgress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_gravity="center"
android:background="?riotx_header_panel_background"
android:indeterminate="true" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/spacePreviewRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fastScrollEnabled="true"
android:overScrollMode="always"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spacePreviewPeekingProgress"
tools:listitem="@layout/item_space_subspace" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?list_divider_color"/>
<LinearLayout
android:id="@+id/spacePreviewButtonBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:elevation="2dp"
android:orientation="horizontal"
android:layout_margin="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/spacePreviewDeclineInviteButton"
style="@style/VectorButtonStyleDestructive"
android:layout_width="0dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:text="@string/decline" />
<com.google.android.material.button.MaterialButton
android:id="@+id/spacePreviewAcceptInviteButton"
style="@style/VectorButtonStylePositive"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/accept" />
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<im.vector.app.features.spaces.preview.SpaceTabView
android:id="@+id/spaceChildTabView"
android:layout_width="wrap_content"
android:layout_marginStart="8dp"
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="6dp"
android:background="@drawable/space_home_background"
android:clipToPadding="false"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/childRoomAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
android:scaleType="centerInside"
android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingPrefix"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/childRoomName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/childRoomTopic"
app:layout_constraintEnd_toStartOf="@id/spaceChildMemberCountIcon"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/childRoomAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/childRoomTopic"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/spaceChildBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/childRoomName"
app:layout_constraintTop_toBottomOf="@+id/childRoomName"
tools:text="@sample/matrix.json/data/roomTopic" />
<ImageView
android:id="@+id/spaceChildMemberCountIcon"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="4dp"
android:src="@drawable/ic_room_profile_member_list"
app:layout_constraintBottom_toBottomOf="@+id/spaceChildMemberCountText"
app:layout_constraintEnd_toStartOf="@+id/spaceChildMemberCountText"
app:layout_constraintTop_toTopOf="@+id/spaceChildMemberCountText"
app:tint="?riotx_text_secondary" />
<TextView
android:id="@+id/spaceChildMemberCountText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:maxLength="5"
android:textColor="?riotx_text_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="123" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/spaceChildBarrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="childRoomAvatar,childRoomTopic"
app:layout_constraintBottom_toBottomOf="parent" />
<Space
android:layout_width="match_parent"
android:layout_height="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/spaceChildBarrier" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="?riotx_background"
android:clipToPadding="false"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<im.vector.app.features.spaces.preview.SpaceTabView
android:id="@+id/childSpaceTab"
android:layout_width="3dp"
android:layout_height="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/childSpaceAvatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/childSpaceAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="centerInside"
android:visibility="visible"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="@id/childSpaceName"
app:layout_constraintStart_toEndOf="@id/childSpaceTab"
app:layout_constraintTop_toTopOf="@id/childSpaceName"
tools:ignore="MissingPrefix"
tools:src="@tools:sample/avatars" />
<View
android:layout_width="2dp"
android:layout_height="0dp"
android:layout_marginTop="2dp"
android:background="?riotx_list_bottom_sheet_divider_color"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="15dp"
app:layout_constraintTop_toBottomOf="@id/childSpaceAvatar">
</View>
<TextView
android:id="@+id/childSpaceName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/childSpaceAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="16dp"
android:layout_height="match_parent">
<View
android:layout_width="2dp"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="?riotx_list_bottom_sheet_divider_color" />
</RelativeLayout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/spaceSummaryMemberCountIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_room_profile_member_list"
app:layout_constraintBottom_toBottomOf="@+id/spaceSummaryMemberCountText"
app:layout_constraintStart_toEndOf="@+id/spaceSummaryMemberCountText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_secondary" />
<TextView
android:id="@+id/spaceSummaryMemberCountText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="?riotx_text_secondary"
app:layout_constraintBottom_toBottomOf="@+id/spaceSummaryMemberCountIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/spaceSummaryMemberCountIcon"
app:layout_constraintTop_toTopOf="parent"
tools:text="123 members" />
<TextView
android:id="@+id/spaceSummaryTopic"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?riotx_text_primary"
android:autoLink="all"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spaceSummaryMemberCountIcon"
tools:text="@sample/matrix.json/data/roomTopic" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -508,6 +508,9 @@
<string name="groups_header">Communities</string>
<string name="no_group_placeholder">No groups</string>
<string name="spaces_invited_header">Invites</string>
<string name="spaces_header">Spaces</string>
<string name="send_bug_report_include_logs">Send logs</string>
<string name="send_bug_report_include_crash_logs">Send crash logs</string>
<string name="send_bug_report_include_key_share_history">Send key share requests history</string>