Room navigation : reuse accept/decline presentation logic
This commit is contained in:
parent
5b8690d32e
commit
9ff0e8eccb
23 changed files with 220 additions and 79 deletions
|
|
@ -350,6 +350,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onInviteClicked(roomId: RoomId) {
|
||||||
|
backstack.push(NavTarget.Room(roomId))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onInviteAccepted(roomId: RoomId) {
|
override fun onInviteAccepted(roomId: RoomId) {
|
||||||
backstack.push(NavTarget.Room(roomId))
|
backstack.push(NavTarget.Room(roomId))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
@ -34,6 +35,7 @@ class JoinRoomNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: JoinRoomPresenter.Factory,
|
presenterFactory: JoinRoomPresenter.Factory,
|
||||||
|
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
|
|
@ -51,5 +53,11 @@ class JoinRoomNode @AssistedInject constructor(
|
||||||
onBackPressed = ::navigateUp,
|
onBackPressed = ::navigateUp,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
acceptDeclineInviteView.Render(
|
||||||
|
state = state.acceptDeclineInviteState,
|
||||||
|
onInviteAccepted = {},
|
||||||
|
onInviteDeclined = { navigateUp() },
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,17 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter
|
||||||
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomId: RoomId,
|
@Assisted private val roomId: RoomId,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val roomListService: RoomListService,
|
private val roomListService: RoomListService,
|
||||||
|
private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter,
|
||||||
) : Presenter<JoinRoomState> {
|
) : Presenter<JoinRoomState> {
|
||||||
|
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -47,6 +49,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
override fun present(): JoinRoomState {
|
override fun present(): JoinRoomState {
|
||||||
val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty())
|
val userMembership by roomListService.getUserMembershipForRoom(roomId).collectAsState(initial = Optional.empty())
|
||||||
val joinAuthorisationStatus = joinAuthorisationStatus(userMembership)
|
val joinAuthorisationStatus = joinAuthorisationStatus(userMembership)
|
||||||
|
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||||
val roomInfo by produceState<AsyncData<RoomInfo>>(initialValue = AsyncData.Uninitialized, key1 = userMembership) {
|
val roomInfo by produceState<AsyncData<RoomInfo>>(initialValue = AsyncData.Uninitialized, key1 = userMembership) {
|
||||||
value = when {
|
value = when {
|
||||||
userMembership.isPresent -> {
|
userMembership.isPresent -> {
|
||||||
|
|
@ -56,6 +59,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
roomName = it.displayName,
|
roomName = it.displayName,
|
||||||
roomAlias = it.alias,
|
roomAlias = it.alias,
|
||||||
memberCount = it.activeMemberCount,
|
memberCount = it.activeMemberCount,
|
||||||
|
isDirect = it.isDirect,
|
||||||
roomAvatarUrl = it.avatarUrl
|
roomAvatarUrl = it.avatarUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -65,21 +69,17 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
fun handleEvents(event: JoinRoomEvents) {
|
fun handleEvents(event: JoinRoomEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> {
|
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> {
|
||||||
coroutineScope.launch {
|
acceptDeclineInviteState.eventSink(
|
||||||
matrixClient.joinRoom(roomId)
|
AcceptDeclineInviteEvents.AcceptInvite(roomInfo.toInviteData())
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
JoinRoomEvents.DeclineInvite -> {
|
JoinRoomEvents.DeclineInvite -> {
|
||||||
coroutineScope.launch {
|
acceptDeclineInviteState.eventSink(
|
||||||
matrixClient.getRoom(roomId)?.use {
|
AcceptDeclineInviteEvents.DeclineInvite(roomInfo.toInviteData())
|
||||||
it.leave()
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,11 +87,21 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
return JoinRoomState(
|
return JoinRoomState(
|
||||||
roomInfo = roomInfo,
|
roomInfo = roomInfo,
|
||||||
joinAuthorisationStatus = joinAuthorisationStatus,
|
joinAuthorisationStatus = joinAuthorisationStatus,
|
||||||
currentAction = CurrentAction.None,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun AsyncData<RoomInfo>.toInviteData(): InviteData {
|
||||||
|
return dataOrNull().let {
|
||||||
|
InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = it?.roomName ?: "",
|
||||||
|
isDirect = it?.isDirect ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun joinAuthorisationStatus(userMembership: Optional<CurrentUserMembership>): JoinAuthorisationStatus {
|
private fun joinAuthorisationStatus(userMembership: Optional<CurrentUserMembership>): JoinAuthorisationStatus {
|
||||||
return when {
|
return when {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.appnav.room.join
|
package io.element.android.appnav.room.join
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -26,7 +27,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
data class JoinRoomState(
|
data class JoinRoomState(
|
||||||
val roomInfo: AsyncData<RoomInfo>,
|
val roomInfo: AsyncData<RoomInfo>,
|
||||||
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
||||||
val currentAction: CurrentAction,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
val eventSink: (JoinRoomEvents) -> Unit
|
val eventSink: (JoinRoomEvents) -> Unit
|
||||||
){
|
){
|
||||||
val showMemberCount = roomInfo.dataOrNull()?.memberCount != null
|
val showMemberCount = roomInfo.dataOrNull()?.memberCount != null
|
||||||
|
|
@ -37,6 +38,7 @@ data class RoomInfo(
|
||||||
val roomName: String,
|
val roomName: String,
|
||||||
val roomAlias: String?,
|
val roomAlias: String?,
|
||||||
val memberCount: Long?,
|
val memberCount: Long?,
|
||||||
|
val isDirect: Boolean,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
) {
|
) {
|
||||||
fun avatarData(size: AvatarSize): AvatarData {
|
fun avatarData(size: AvatarSize): AvatarData {
|
||||||
|
|
@ -55,7 +57,3 @@ enum class JoinAuthorisationStatus {
|
||||||
CanJoin,
|
CanJoin,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface CurrentAction {
|
|
||||||
data object None : CurrentAction
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package io.element.android.appnav.room.join
|
package io.element.android.appnav.room.join
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
|
@ -45,16 +47,17 @@ fun aJoinRoomState(
|
||||||
roomName = "Element x android",
|
roomName = "Element x android",
|
||||||
roomAlias = "#exa:matrix.org",
|
roomAlias = "#exa:matrix.org",
|
||||||
memberCount = null,
|
memberCount = null,
|
||||||
|
isDirect = false,
|
||||||
roomAvatarUrl = null
|
roomAvatarUrl = null
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown,
|
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown,
|
||||||
currentAction: CurrentAction = CurrentAction.None,
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||||
) = JoinRoomState(
|
) = JoinRoomState(
|
||||||
roomInfo = roomInfo,
|
roomInfo = roomInfo,
|
||||||
joinAuthorisationStatus = joinAuthorisationStatus,
|
joinAuthorisationStatus = joinAuthorisationStatus,
|
||||||
currentAction = currentAction,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.appnav.room.join.JoinRoomPresenter
|
import io.element.android.appnav.room.join.JoinRoomPresenter
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -32,13 +33,15 @@ object JoinRoomModule {
|
||||||
fun providesJoinRoomPresenterFactory(
|
fun providesJoinRoomPresenterFactory(
|
||||||
roomListService: RoomListService,
|
roomListService: RoomListService,
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
|
acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter,
|
||||||
): JoinRoomPresenter.Factory {
|
): JoinRoomPresenter.Factory {
|
||||||
return object : JoinRoomPresenter.Factory {
|
return object : JoinRoomPresenter.Factory {
|
||||||
override fun create(roomId: RoomId): JoinRoomPresenter {
|
override fun create(roomId: RoomId): JoinRoomPresenter {
|
||||||
return JoinRoomPresenter(
|
return JoinRoomPresenter(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
matrixClient = client,
|
matrixClient = client,
|
||||||
roomListService = roomListService
|
roomListService = roomListService,
|
||||||
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-compose-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ interface InviteListEntryPoint : FeatureEntryPoint {
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onBackClicked()
|
fun onBackClicked()
|
||||||
|
fun onInviteClicked(roomId: RoomId)
|
||||||
fun onInviteAccepted(roomId: RoomId)
|
fun onInviteAccepted(roomId: RoomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
package io.element.android.features.invite.api.response
|
||||||
|
|
||||||
sealed interface AcceptDeclineInviteEvents {
|
interface AcceptDeclineInviteEvents {
|
||||||
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
||||||
data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
||||||
data object ConfirmDeclineInvite : AcceptDeclineInviteEvents
|
|
||||||
data object CancelDeclineInvite : AcceptDeclineInviteEvents
|
|
||||||
data object DismissAcceptError : AcceptDeclineInviteEvents
|
|
||||||
data object DismissDeclineError : AcceptDeclineInviteEvents
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.invite.api.response
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
|
interface AcceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
package io.element.android.features.invite.api.response
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -24,11 +24,5 @@ data class AcceptDeclineInviteState(
|
||||||
val invite: Optional<InviteData>,
|
val invite: Optional<InviteData>,
|
||||||
val acceptAction: AsyncAction<RoomId>,
|
val acceptAction: AsyncAction<RoomId>,
|
||||||
val declineAction: AsyncAction<RoomId>,
|
val declineAction: AsyncAction<RoomId>,
|
||||||
val eventSink: (AcceptDeclineInviteEvents) -> Unit
|
val eventSink: (AcceptDeclineInviteEvents) -> Unit,
|
||||||
)
|
|
||||||
|
|
||||||
data class InviteData(
|
|
||||||
val roomId: RoomId,
|
|
||||||
val roomName: String,
|
|
||||||
val isDirect: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
package io.element.android.features.invite.api.response
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.invite.api.response
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
interface AcceptDeclineInviteView {
|
||||||
|
@Composable
|
||||||
|
fun Render(
|
||||||
|
state: AcceptDeclineInviteState,
|
||||||
|
onInviteAccepted: (RoomId) -> Unit,
|
||||||
|
onInviteDeclined: (RoomId) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.invite.api.response
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
data class InviteData(
|
||||||
|
val roomId: RoomId,
|
||||||
|
val roomName: String,
|
||||||
|
val isDirect: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -43,6 +43,10 @@ class InviteListNode @AssistedInject constructor(
|
||||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onInviteClicked(roomId: RoomId) {
|
||||||
|
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteClicked(roomId) }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -50,7 +54,8 @@ class InviteListNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClicked = ::onBackClicked,
|
onBackClicked = ::onBackClicked,
|
||||||
onInviteAccepted = ::onInviteAccepted,
|
onInviteAccepted = ::onInviteAccepted,
|
||||||
onInviteDeclined = {}
|
onInviteDeclined = {},
|
||||||
|
onInviteClicked = ::onInviteClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter
|
||||||
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
import io.element.android.features.invite.impl.model.InviteSender
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteEvents
|
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
|
|
||||||
import io.element.android.features.invite.impl.response.InviteData
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package io.element.android.features.invite.impl.invitelist
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
package io.element.android.features.invite.impl.invitelist
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||||
import io.element.android.features.invite.impl.model.InviteSender
|
import io.element.android.features.invite.impl.model.InviteSender
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteStateProvider
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.impl.response.anAcceptDeclineInviteState
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.invitelist
|
package io.element.android.features.invite.impl.invitelist
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
|
@ -53,11 +54,13 @@ fun InviteListView(
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
onInviteAccepted: (RoomId) -> Unit,
|
onInviteAccepted: (RoomId) -> Unit,
|
||||||
onInviteDeclined: (RoomId) -> Unit,
|
onInviteDeclined: (RoomId) -> Unit,
|
||||||
|
onInviteClicked: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
InviteListContent(
|
InviteListContent(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
onInviteClicked = onInviteClicked,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
)
|
)
|
||||||
AcceptDeclineInviteView(
|
AcceptDeclineInviteView(
|
||||||
|
|
@ -72,6 +75,7 @@ fun InviteListView(
|
||||||
private fun InviteListContent(
|
private fun InviteListContent(
|
||||||
state: InviteListState,
|
state: InviteListState,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
onInviteClicked: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -112,6 +116,9 @@ private fun InviteListContent(
|
||||||
items = state.inviteList,
|
items = state.inviteList,
|
||||||
) { index, invite ->
|
) { index, invite ->
|
||||||
InviteSummaryRow(
|
InviteSummaryRow(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
onClick = { onInviteClicked(invite.roomId) }
|
||||||
|
),
|
||||||
invite = invite,
|
invite = invite,
|
||||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
||||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
||||||
|
|
@ -136,5 +143,6 @@ internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::cl
|
||||||
onBackClicked = {},
|
onBackClicked = {},
|
||||||
onInviteAccepted = {},
|
onInviteAccepted = {},
|
||||||
onInviteDeclined = {},
|
onInviteDeclined = {},
|
||||||
|
onInviteClicked = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
||||||
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.features.invite.impl.R
|
import io.element.android.features.invite.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
|
|
@ -42,14 +45,14 @@ fun AcceptDeclineInviteView(
|
||||||
async = state.acceptAction,
|
async = state.acceptAction,
|
||||||
onSuccess = onInviteAccepted,
|
onSuccess = onInviteAccepted,
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(AcceptDeclineInviteEvents.DismissAcceptError)
|
state.eventSink(DefaultAcceptDeclineInviteEvents.DismissAcceptError)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.declineAction,
|
async = state.declineAction,
|
||||||
onSuccess = onInviteDeclined,
|
onSuccess = onInviteDeclined,
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(AcceptDeclineInviteEvents.DismissDeclineError)
|
state.eventSink(DefaultAcceptDeclineInviteEvents.DismissDeclineError)
|
||||||
},
|
},
|
||||||
confirmationDialog = {
|
confirmationDialog = {
|
||||||
val invite = state.invite.getOrNull()
|
val invite = state.invite.getOrNull()
|
||||||
|
|
@ -57,10 +60,10 @@ fun AcceptDeclineInviteView(
|
||||||
DeclineConfirmationDialog(
|
DeclineConfirmationDialog(
|
||||||
invite = invite,
|
invite = invite,
|
||||||
onConfirmClicked = {
|
onConfirmClicked = {
|
||||||
state.eventSink(AcceptDeclineInviteEvents.ConfirmDeclineInvite)
|
state.eventSink(DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite)
|
||||||
},
|
},
|
||||||
onDismissClicked = {
|
onDismissClicked = {
|
||||||
state.eventSink(AcceptDeclineInviteEvents.CancelDeclineInvite)
|
state.eventSink(DefaultAcceptDeclineInviteEvents.CancelDeclineInvite)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.invite.impl.response
|
||||||
|
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
|
||||||
|
sealed interface DefaultAcceptDeclineInviteEvents: AcceptDeclineInviteEvents {
|
||||||
|
data object ConfirmDeclineInvite : DefaultAcceptDeclineInviteEvents
|
||||||
|
data object CancelDeclineInvite : DefaultAcceptDeclineInviteEvents
|
||||||
|
data object DismissAcceptError : DefaultAcceptDeclineInviteEvents
|
||||||
|
data object DismissDeclineError : DefaultAcceptDeclineInviteEvents
|
||||||
|
}
|
||||||
|
|
@ -23,10 +23,16 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter
|
||||||
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
|
|
@ -38,11 +44,12 @@ import java.util.Optional
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.jvm.optionals.getOrNull
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
class AcceptDeclineInvitePresenter @Inject constructor(
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultAcceptDeclineInvitePresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
) : Presenter<AcceptDeclineInviteState> {
|
) : AcceptDeclineInvitePresenter {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AcceptDeclineInviteState {
|
override fun present(): AcceptDeclineInviteState {
|
||||||
|
|
@ -66,7 +73,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
declinedAction.value = AsyncAction.Confirming
|
declinedAction.value = AsyncAction.Confirming
|
||||||
}
|
}
|
||||||
|
|
||||||
is AcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
|
is DefaultAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
|
||||||
declinedAction.value = AsyncAction.Uninitialized
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
currentInvite.getOrNull()?.let {
|
currentInvite.getOrNull()?.let {
|
||||||
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
||||||
|
|
@ -74,16 +81,16 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
currentInvite = Optional.empty()
|
currentInvite = Optional.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
is AcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
is DefaultAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
||||||
currentInvite = Optional.empty()
|
currentInvite = Optional.empty()
|
||||||
declinedAction.value = AsyncAction.Uninitialized
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
is AcceptDeclineInviteEvents.DismissAcceptError -> {
|
is DefaultAcceptDeclineInviteEvents.DismissAcceptError -> {
|
||||||
acceptedAction.value = AsyncAction.Uninitialized
|
acceptedAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
is AcceptDeclineInviteEvents.DismissDeclineError -> {
|
is DefaultAcceptDeclineInviteEvents.DismissDeclineError -> {
|
||||||
declinedAction.value = AsyncAction.Uninitialized
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,14 +105,14 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||||
suspend {
|
acceptedAction.runUpdatingState {
|
||||||
client.getRoom(roomId)?.use {
|
client.joinRoom(roomId).onSuccess {
|
||||||
it.join().getOrThrow()
|
|
||||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||||
analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
client.getRoom(roomId)?.use { room ->
|
||||||
|
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
roomId
|
}
|
||||||
}.runCatchingUpdatingState(acceptedAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||||
|
|
@ -18,28 +18,27 @@ package io.element.android.features.invite.impl.response
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import com.bumble.appyx.core.node.Node
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class AcceptDeclineInviteNode @AssistedInject constructor(
|
class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInviteView {
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
private val presenter: AcceptDeclineInvitePresenter,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun Render(
|
||||||
val state = presenter.present()
|
state: AcceptDeclineInviteState,
|
||||||
|
onInviteAccepted: (RoomId) -> Unit,
|
||||||
|
onInviteDeclined: (RoomId) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
AcceptDeclineInviteView(
|
AcceptDeclineInviteView(
|
||||||
state = state,
|
state = state,
|
||||||
onInviteAccepted = {},
|
onInviteAccepted = onInviteAccepted,
|
||||||
onInviteDeclined = {},
|
onInviteDeclined = onInviteDeclined,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue