Skip to content

Commit

Permalink
Show badges for new invites (#355)
Browse files Browse the repository at this point in the history
Show badges for new invites

Closes #238
  • Loading branch information
csmith authored May 4, 2023
1 parent 967182f commit a5644ba
Show file tree
Hide file tree
Showing 49 changed files with 746 additions and 155 deletions.
1 change: 1 addition & 0 deletions changelog.d/238.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Create and join rooms] New invites are now marked with a badge
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 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 io.element.android.features.invitelist.api

import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow

interface SeenInvitesStore {
fun seenRoomIds(): Flow<Set<RoomId>>
suspend fun markAsSeen(roomIds: Set<RoomId>)
}
2 changes: 2 additions & 0 deletions features/invitelist/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.invitelist.api)
implementation(libs.androidx.datastore.preferences)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
Expand All @@ -48,6 +49,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.invitelist.test)

ksp(libs.showkase.processor)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 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 io.element.android.features.invitelist.impl

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invitelist.api.SeenInvitesStore
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_seeninvites")
private val seenInvitesKey = stringSetPreferencesKey("seenInvites")


@ContributesBinding(SessionScope::class)
class DefaultSeenInvitesStore @Inject constructor(
@ApplicationContext context: Context
) : SeenInvitesStore {

private val store = context.dataStore

override fun seenRoomIds(): Flow<Set<RoomId>> =
store.data.map { prefs ->
prefs[seenInvitesKey]
.orEmpty()
.map { RoomId(it) }
.toSet()
}

override suspend fun markAsSeen(roomIds: Set<RoomId>) {
store.edit { prefs ->
prefs[seenInvitesKey] = roomIds.map { it.value }.toSet()
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
package io.element.android.features.invitelist.impl

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.features.invitelist.api.SeenInvitesStore
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.architecture.Async
Expand All @@ -34,11 +37,13 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummary
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject

class InviteListPresenter @Inject constructor(
private val client: MatrixClient,
private val store: SeenInvitesStore,
) : Presenter<InviteListState> {

@Composable
Expand All @@ -48,6 +53,21 @@ class InviteListPresenter @Inject constructor(
.roomSummaries()
.collectAsState()

var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }

LaunchedEffect(Unit) {
seenInvites = store.seenRoomIds().first()
}

LaunchedEffect(invites) {
store.markAsSeen(
invites
.filterIsInstance<RoomSummary.Filled>()
.map { it.details.roomId }
.toSet()
)
}

val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
val declinedAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
Expand Down Expand Up @@ -86,8 +106,17 @@ class InviteListPresenter @Inject constructor(
}
}

val inviteList = remember(seenInvites, invites) {
invites
.filterIsInstance<RoomSummary.Filled>()
.map {
it.toInviteSummary(seenInvites.contains(it.details.roomId))
}
.toPersistentList()
}

return InviteListState(
inviteList = invites.mapNotNull(::toInviteSummary).toPersistentList(),
inviteList = inviteList,
declineConfirmationDialog = decliningInvite.value?.let {
InviteDeclineConfirmationDialog.Visible(
isDirect = it.isDirect,
Expand All @@ -113,49 +142,44 @@ class InviteListPresenter @Inject constructor(
}.execute(declinedAction)
}

private fun toInviteSummary(roomSummary: RoomSummary): InviteListInviteSummary? {
return when (roomSummary) {
is RoomSummary.Filled -> roomSummary.details.run {
val i = inviter
val avatarData = if (isDirect && i != null)
AvatarData(
id = i.userId.value,
name = i.displayName,
url = i.avatarUrl,
)
else
AvatarData(
id = roomId.value,
name = name,
url = avatarURLString
)

val alias = if (isDirect)
inviter?.userId?.value
else
canonicalAlias

InviteListInviteSummary(
roomId = roomId,
roomName = name,
roomAlias = alias,
roomAvatarData = avatarData,
isDirect = isDirect,
sender = if (isDirect) null else inviter?.let {
InviteSender(
userId = it.userId,
displayName = it.displayName ?: "",
avatarData = AvatarData(
id = it.userId.value,
name = it.displayName,
url = it.avatarUrl,
),
)
},
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
val i = inviter
val avatarData = if (isDirect && i != null)
AvatarData(
id = i.userId.value,
name = i.displayName,
url = i.avatarUrl,
)
else
AvatarData(
id = roomId.value,
name = name,
url = avatarURLString
)

val alias = if (isDirect)
inviter?.userId?.value
else
canonicalAlias

InviteListInviteSummary(
roomId = roomId,
roomName = name,
roomAlias = alias,
roomAvatarData = avatarData,
isDirect = isDirect,
isNew = !seen,
sender = if (isDirect) null else inviter?.run {
InviteSender(
userId = userId,
displayName = displayName ?: "",
avatarData = AvatarData(
id = userId.value,
name = displayName,
url = avatarUrl,
),
)
}

else -> null
}
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.element.android.features.invitelist.impl.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
Expand All @@ -27,13 +28,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.Placeholder
Expand All @@ -59,6 +64,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
import kotlinx.collections.immutable.persistentMapOf
import io.element.android.libraries.ui.strings.R as StringR

Expand All @@ -73,8 +79,8 @@ internal fun InviteSummaryRow(
) {
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = minHeight)
.fillMaxWidth()
.heightIn(min = minHeight)
) {
DefaultInviteSummaryRow(
invite = invite,
Expand All @@ -92,19 +98,20 @@ internal fun DefaultInviteSummaryRow(
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.height(IntrinsicSize.Min),
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.Top
) {
Avatar(
invite.roomAvatarData,
)

Column(
modifier = Modifier
.padding(start = 12.dp, end = 4.dp)
.alignByBaseline()
.weight(1f)
.padding(start = 12.dp, end = 4.dp)
.alignByBaseline()
.weight(1f)
) {
// Name
Text(
Expand Down Expand Up @@ -152,6 +159,15 @@ internal fun DefaultInviteSummaryRow(
)
}
}

val unreadIndicatorColor = if (invite.isNew) MaterialTheme.roomListUnreadIndicator() else Color.Transparent

Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(unreadIndicatorColor)
)
}
}

Expand Down Expand Up @@ -183,7 +199,11 @@ private fun SenderRow(sender: InviteSender) {
Placeholder(20.dp.toSp(), 20.dp.toSp(), PlaceholderVerticalAlign.Center)
}
) {
Box(Modifier.fillMaxHeight().padding(end = 4.dp)) {
Box(
Modifier
.fillMaxHeight()
.padding(end = 4.dp)
) {
Avatar(
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
modifier = Modifier.align(Alignment.Center)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ data class InviteListInviteSummary(
val roomAlias: String? = null,
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName),
val sender: InviteSender? = null,
val isDirect: Boolean = false
val isDirect: Boolean = false,
val isNew: Boolean = false,
)

data class InviteSender(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteList
aInviteListInviteSummary(),
aInviteListInviteSummary().copy(roomAlias = "#someroom:example.com"),
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
aInviteListInviteSummary().copy(isNew = true)
)
}

Expand Down
Loading

0 comments on commit a5644ba

Please sign in to comment.