Merge pull request #2930 from element-hq/feature/bma/blockedUserData
Render blocked user data (behind a disabled feature flag)
This commit is contained in:
commit
f74032d87a
23 changed files with 226 additions and 45 deletions
1
changelog.d/2930.misc
Normal file
1
changelog.d/2930.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add a feature flag ShowBlockedUsersDetails, disabled by default to render display name and avatar of blocked users in the blocked users list.
|
||||
|
|
@ -21,20 +21,26 @@ import androidx.compose.runtime.MutableState
|
|||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class BlockedUsersPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<BlockedUsersState> {
|
||||
@Composable
|
||||
override fun present(): BlockedUsersState {
|
||||
|
|
@ -47,7 +53,24 @@ class BlockedUsersPresenter @Inject constructor(
|
|||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
val renderBlockedUsersDetail = featureFlagService
|
||||
.isFeatureEnabledFlow(FeatureFlags.ShowBlockedUsersDetails)
|
||||
.collectAsState(initial = false)
|
||||
val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState()
|
||||
val ignoredMatrixUser by produceState(
|
||||
initialValue = ignoredUserIds.map { MatrixUser(userId = it) },
|
||||
key1 = renderBlockedUsersDetail.value,
|
||||
key2 = ignoredUserIds
|
||||
) {
|
||||
value = ignoredUserIds.map {
|
||||
if (renderBlockedUsersDetail.value) {
|
||||
matrixClient.getProfile(it).getOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
?: MatrixUser(userId = it)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: BlockedUsersEvents) {
|
||||
when (event) {
|
||||
|
|
@ -68,7 +91,7 @@ class BlockedUsersPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
return BlockedUsersState(
|
||||
blockedUsers = ignoredUserIds,
|
||||
blockedUsers = ignoredMatrixUser.toPersistentList(),
|
||||
unblockUserAction = unblockUserAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package io.element.android.features.preferences.impl.blockedusers
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class BlockedUsersState(
|
||||
val blockedUsers: ImmutableList<UserId>,
|
||||
val blockedUsers: ImmutableList<MatrixUser>,
|
||||
val unblockUserAction: AsyncAction<Unit>,
|
||||
val eventSink: (BlockedUsersEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.preferences.impl.blockedusers
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
|
|
@ -26,10 +26,9 @@ class BlockedUsersStatePreviewProvider : PreviewParameterProvider<BlockedUsersSt
|
|||
override val values: Sequence<BlockedUsersState>
|
||||
get() = sequenceOf(
|
||||
aBlockedUsersState(),
|
||||
aBlockedUsersState(blockedUsers = aMatrixUserList().map { it.copy(displayName = null, avatarUrl = null) }),
|
||||
aBlockedUsersState(blockedUsers = emptyList()),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Confirming),
|
||||
// Sadly there's no good way to preview Loading or Failure states since they're presented with an animation
|
||||
// All these 3 screen states will be displayed as the Uninitialized one
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Loading),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Failure(Throwable("Failed to unblock user"))),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Success(Unit)),
|
||||
|
|
@ -37,12 +36,13 @@ class BlockedUsersStatePreviewProvider : PreviewParameterProvider<BlockedUsersSt
|
|||
}
|
||||
|
||||
internal fun aBlockedUsersState(
|
||||
blockedUsers: List<UserId> = aMatrixUserList().map { it.userId },
|
||||
blockedUsers: List<MatrixUser> = aMatrixUserList(),
|
||||
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (BlockedUsersEvents) -> Unit = {},
|
||||
): BlockedUsersState {
|
||||
return BlockedUsersState(
|
||||
blockedUsers = blockedUsers.toPersistentList(),
|
||||
unblockUserAction = unblockUserAction,
|
||||
eventSink = {},
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@ fun BlockedUsersView(
|
|||
LazyColumn(
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
items(state.blockedUsers) { userId ->
|
||||
items(state.blockedUsers) { matrixUser ->
|
||||
BlockedUserItem(
|
||||
userId = userId,
|
||||
matrixUser = matrixUser,
|
||||
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) }
|
||||
)
|
||||
}
|
||||
|
|
@ -121,12 +121,12 @@ fun BlockedUsersView(
|
|||
|
||||
@Composable
|
||||
private fun BlockedUserItem(
|
||||
userId: UserId,
|
||||
matrixUser: MatrixUser,
|
||||
onClick: (UserId) -> Unit,
|
||||
) {
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable { onClick(userId) },
|
||||
matrixUser = MatrixUser(userId),
|
||||
modifier = Modifier.clickable { onClick(matrixUser.userId) },
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BlockedUserViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes back callback`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClicked = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a user emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
val userList = aMatrixUserList()
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
blockedUsers = userList,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(userList.first().displayName.orEmpty()).performClick()
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel sends a BlockedUsersEvents`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
unblockUserAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.Cancel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on confirm sends a BlockedUsersEvents`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
unblockUserAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_blocked_users_unblock_alert_action)
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLogoutView(
|
||||
state: BlockedUsersState,
|
||||
onBackClicked: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
BlockedUsersView(
|
||||
state = state,
|
||||
onBackPressed = onBackClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,11 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
|
|
@ -52,7 +57,7 @@ class BlockedUsersPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID))
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(MatrixUser(A_USER_ID)))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
|
@ -68,14 +73,39 @@ class BlockedUsersPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).containsAtLeastElementsIn(persistentListOf(A_USER_ID))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID)))
|
||||
}
|
||||
|
||||
matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID, A_USER_ID_2))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - blocked users list with data`() = runTest {
|
||||
val alice = MatrixUser(A_USER_ID, displayName = "Alice", avatarUrl = "aliceAvatar")
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
|
||||
givenGetProfileResult(A_USER_ID, Result.success(alice))
|
||||
givenGetProfileResult(A_USER_ID_2, Result.failure(AN_EXCEPTION))
|
||||
}
|
||||
val presenter = aBlockedUsersPresenter(
|
||||
matrixClient = matrixClient,
|
||||
featureFlagService = FakeFeatureFlagService().apply {
|
||||
setFeatureEnabled(FeatureFlags.ShowBlockedUsersDetails, true)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2)))
|
||||
}
|
||||
// Alice is resolved
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(listOf(alice, MatrixUser(A_USER_ID_2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -157,5 +187,9 @@ class BlockedUsersPresenterTests {
|
|||
|
||||
private fun aBlockedUsersPresenter(
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
) = BlockedUsersPresenter(matrixClient)
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
) = BlockedUsersPresenter(
|
||||
matrixClient = matrixClient,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,12 @@ enum class FeatureFlags(
|
|||
description = "Allow user to search for public rooms in their homeserver",
|
||||
defaultValue = false,
|
||||
isFinished = false,
|
||||
)
|
||||
),
|
||||
ShowBlockedUsersDetails(
|
||||
key = "feature.showBlockedUsersDetails",
|
||||
title = "Show blocked users details",
|
||||
description = "Show the name and avatar of blocked users in the blocked users list",
|
||||
defaultValue = false,
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
|||
FeatureFlags.Mentions -> true
|
||||
FeatureFlags.MarkAsUnread -> true
|
||||
FeatureFlags.RoomDirectorySearch -> false
|
||||
FeatureFlags.ShowBlockedUsersDetails -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d0cb8cd165599d1bae48bc9b6334e519cdba89287756b65caae86cd268667f40
|
||||
size 62939
|
||||
oid sha256:0268cabfadfeda384616b4d93895d170efa8360c81de2f729111dfc1c4c76da0
|
||||
size 62874
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c9e8374a634bf28b84e9945596a0c144971f550f8c1cd57abc0f4db30556fe5
|
||||
size 8959
|
||||
oid sha256:d0cb8cd165599d1bae48bc9b6334e519cdba89287756b65caae86cd268667f40
|
||||
size 62939
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:709a470ac8764fee25eca83978daf039c089f2e8073b22afc1aa08fcf0d69998
|
||||
size 60072
|
||||
oid sha256:8c9e8374a634bf28b84e9945596a0c144971f550f8c1cd57abc0f4db30556fe5
|
||||
size 8959
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a515efeb80f1380d661f8531f0c85bb46abd71bf535e050444fbb9f3e72f7011
|
||||
size 65964
|
||||
oid sha256:1a36e3680e98e899f23c3c208f8f51ed6195b99ee0ccd5f7fe4b67992ca1af8e
|
||||
size 60700
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9a47cd564da066d38db3e26f7b80b2a6e9a63ab276a2af688bee10bae462e775
|
||||
size 65580
|
||||
oid sha256:f69003da2a20cc3aa6c5a9695fc5dbece0cfa266b0c0c784249a40eecf854925
|
||||
size 65905
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d0cb8cd165599d1bae48bc9b6334e519cdba89287756b65caae86cd268667f40
|
||||
size 62939
|
||||
oid sha256:92019951732c798875716694ac2495a72135413501d1fc28e1162efa1a717da0
|
||||
size 65524
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0268cabfadfeda384616b4d93895d170efa8360c81de2f729111dfc1c4c76da0
|
||||
size 62874
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:faa8c0cfc37478b8396d40c793ee3388fadfea8463c5f7cc4db1f5cc2cf10144
|
||||
size 58891
|
||||
oid sha256:ed2277c886df7537766880554569b94e9587821253f0d62a0f25c760cfecb427
|
||||
size 62315
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cc12e1a6ebd2340718bfb70a5ceb265c526cef8c1fac285e1e6ec924a7c12324
|
||||
size 8393
|
||||
oid sha256:faa8c0cfc37478b8396d40c793ee3388fadfea8463c5f7cc4db1f5cc2cf10144
|
||||
size 58891
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc5c62caa2da7e51d035ffc7a00d9d3abaeee06cbd48c3d0a6e7e3226c04e4e4
|
||||
size 55857
|
||||
oid sha256:cc12e1a6ebd2340718bfb70a5ceb265c526cef8c1fac285e1e6ec924a7c12324
|
||||
size 8393
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6974b5d06b3d8b64bf26c3b0b48cd57a2c2e38cee6aa8b06bb85d64392e1b685
|
||||
size 60855
|
||||
oid sha256:6c65880e75d0d796a3f122b4896b7134cd93448e3b26e38c7f5b9a920b1eaceb
|
||||
size 56704
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:96053ba2bb1dbbb69cd0ddb7e218ad68401ee1344844508766c5724a08e171fb
|
||||
size 60491
|
||||
oid sha256:1b42ea9c681420e13912166b8d42195931c93e7f0fc69bea711ed2027395c44c
|
||||
size 64232
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:faa8c0cfc37478b8396d40c793ee3388fadfea8463c5f7cc4db1f5cc2cf10144
|
||||
size 58891
|
||||
oid sha256:e72323d61798f93d768e06e86e7e03335297e93fc82bd96083d1a27bfa5a9b78
|
||||
size 63874
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed2277c886df7537766880554569b94e9587821253f0d62a0f25c760cfecb427
|
||||
size 62315
|
||||
Loading…
Add table
Add a link
Reference in a new issue