mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 18:05:36 +03:00
Cleanup
This commit is contained in:
parent
e0ea0c195b
commit
cca6d0e967
13 changed files with 88 additions and 87 deletions
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.Room
|
|||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
||||
|
@ -119,7 +120,7 @@ class RxRoom(private val room: Room) {
|
|||
room.updateCanonicalAlias(alias, it)
|
||||
}
|
||||
|
||||
fun updateHistoryReadability(readability: String) = completableBuilder<Unit> {
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder<Unit> {
|
||||
room.updateHistoryReadability(readability, it)
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData
|
|||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
|
@ -50,7 +51,7 @@ interface StateService {
|
|||
/**
|
||||
* Update the history readability of the room
|
||||
*/
|
||||
fun updateHistoryReadability(readability: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the avatar of the room
|
||||
|
|
|
@ -28,8 +28,6 @@ import im.vector.matrix.android.api.util.JsonDict
|
|||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.database.model.UserThreePidEntity
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.session.content.FileUploader
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
|
@ -38,12 +36,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
|||
import io.realm.kotlin.where
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK"
|
||||
|
||||
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
@SessionId private val sessionId: String,
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
|
||||
private val getProfileInfoTask: GetProfileInfoTask,
|
||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
|||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.state.StateService
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
|
@ -35,8 +36,6 @@ import im.vector.matrix.android.internal.task.configureWith
|
|||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
|
||||
private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK"
|
||||
|
||||
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
|
@ -121,7 +120,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
|||
)
|
||||
}
|
||||
|
||||
override fun updateHistoryReadability(readability: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
body = mapOf("history_visibility" to readability),
|
||||
|
|
|
@ -97,15 +97,15 @@ class BigImageViewerActivity : VectorBaseActivity() {
|
|||
}
|
||||
|
||||
private fun showAvatarSelector() {
|
||||
AlertDialog
|
||||
.Builder(this)
|
||||
AlertDialog.Builder(this)
|
||||
.setItems(arrayOf(
|
||||
stringProvider.getString(R.string.attachment_type_camera),
|
||||
stringProvider.getString(R.string.attachment_type_gallery)
|
||||
)) { dialog, which ->
|
||||
dialog.cancel()
|
||||
onAvatarTypeSelected(isCamera = (which == 0))
|
||||
}.show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private var avatarCameraUri: Uri? = null
|
||||
|
|
|
@ -46,7 +46,6 @@ import im.vector.riotx.core.extensions.exhaustive
|
|||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
|
||||
import im.vector.riotx.core.utils.allGranted
|
||||
|
@ -78,8 +77,7 @@ data class RoomProfileArgs(
|
|||
class RoomProfileFragment @Inject constructor(
|
||||
private val roomProfileController: RoomProfileController,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
val roomProfileViewModelFactory: RoomProfileViewModel.Factory,
|
||||
val colorProvider: ColorProvider
|
||||
val roomProfileViewModelFactory: RoomProfileViewModel.Factory
|
||||
) : VectorBaseFragment(), RoomProfileController.Callback {
|
||||
|
||||
private val roomProfileArgs: RoomProfileArgs by args()
|
||||
|
@ -253,15 +251,15 @@ class RoomProfileFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun showAvatarSelector() {
|
||||
AlertDialog
|
||||
.Builder(requireContext())
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setItems(arrayOf(
|
||||
getString(R.string.attachment_type_camera),
|
||||
getString(R.string.attachment_type_gallery)
|
||||
)) { dialog, which ->
|
||||
dialog.cancel()
|
||||
onAvatarTypeSelected(isCamera = (which == 0))
|
||||
}.show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private var avatarCameraUri: Uri? = null
|
||||
|
|
|
@ -68,10 +68,12 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
|
|||
|
||||
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
|
||||
|
||||
powerLevelsContentLive.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) }
|
||||
}.disposeOnClear()
|
||||
powerLevelsContentLive
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) }
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
override fun handle(action: RoomProfileAction) = when (action) {
|
||||
|
@ -111,11 +113,13 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
|
|||
private fun handleChangeAvatar(action: RoomProfileAction.ChangeRoomAvatar) {
|
||||
_viewEvents.post(RoomProfileViewEvents.Loading())
|
||||
room.rx().updateAvatar(action.uri, action.fileName ?: UUID.randomUUID().toString())
|
||||
.subscribe({
|
||||
_viewEvents.post(RoomProfileViewEvents.OnChangeAvatarSuccess)
|
||||
}, {
|
||||
_viewEvents.post(RoomProfileViewEvents.Failure(it))
|
||||
})
|
||||
.subscribe(
|
||||
{
|
||||
_viewEvents.post(RoomProfileViewEvents.OnChangeAvatarSuccess)
|
||||
},
|
||||
{
|
||||
_viewEvents.post(RoomProfileViewEvents.Failure(it))
|
||||
})
|
||||
.disposeOnClear()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ class RoomSettingsController @Inject constructor(
|
|||
formEditTextItem {
|
||||
id("alias")
|
||||
enabled(data.actionPermissions.canChangeCanonicalAlias)
|
||||
value(data.newAlias ?: roomSummary.canonicalAlias)
|
||||
value(data.newCanonicalAlias ?: roomSummary.canonicalAlias)
|
||||
hint(stringProvider.getString(R.string.room_settings_addresses_add_new_address))
|
||||
|
||||
onTextChange { text ->
|
||||
|
|
|
@ -34,7 +34,6 @@ import im.vector.riotx.core.extensions.cleanup
|
|||
import im.vector.riotx.core.extensions.configureWith
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.roomprofile.RoomProfileArgs
|
||||
|
@ -45,8 +44,7 @@ import javax.inject.Inject
|
|||
class RoomSettingsFragment @Inject constructor(
|
||||
val viewModelFactory: RoomSettingsViewModel.Factory,
|
||||
private val controller: RoomSettingsController,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val stringProvider: StringProvider
|
||||
private val avatarRenderer: AvatarRenderer
|
||||
) : VectorBaseFragment(), RoomSettingsController.Callback {
|
||||
|
||||
private val viewModel: RoomSettingsViewModel by fragmentViewModel()
|
||||
|
@ -154,12 +152,13 @@ class RoomSettingsFragment @Inject constructor(
|
|||
return@withState
|
||||
}
|
||||
|
||||
// TODO Create a formatter for this enum, it's done 3 times in the project
|
||||
private fun formatHistoryVisibility(historyVisibility: RoomHistoryVisibility): String {
|
||||
return when (historyVisibility) {
|
||||
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
||||
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited)
|
||||
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
|
||||
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
|
||||
RoomHistoryVisibility.SHARED -> getString(R.string.notice_room_visibility_shared)
|
||||
RoomHistoryVisibility.INVITED -> getString(R.string.notice_room_visibility_invited)
|
||||
RoomHistoryVisibility.JOINED -> getString(R.string.notice_room_visibility_joined)
|
||||
RoomHistoryVisibility.WORLD_READABLE -> getString(R.string.notice_room_visibility_world_readable)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,11 +27,11 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import java.util.Locale
|
||||
|
||||
class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState,
|
||||
private val session: Session)
|
||||
|
@ -55,6 +55,30 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
|
||||
init {
|
||||
observeRoomSummary()
|
||||
observeState()
|
||||
}
|
||||
|
||||
private fun observeState() {
|
||||
selectSubscribe(
|
||||
RoomSettingsViewState::newName,
|
||||
RoomSettingsViewState::newCanonicalAlias,
|
||||
RoomSettingsViewState::newTopic,
|
||||
RoomSettingsViewState::newHistoryVisibility,
|
||||
RoomSettingsViewState::roomSummary) { newName,
|
||||
newAlias,
|
||||
newTopic,
|
||||
newHistoryVisibility,
|
||||
asyncSummary ->
|
||||
val summary = asyncSummary()
|
||||
setState {
|
||||
copy(
|
||||
showSaveAction = summary?.displayName != newName
|
||||
|| summary?.topic != newTopic
|
||||
|| summary?.canonicalAlias != newAlias
|
||||
|| newHistoryVisibility != null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeRoomSummary() {
|
||||
|
@ -67,53 +91,35 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
roomSummary = async,
|
||||
newName = roomSummary?.displayName,
|
||||
newTopic = roomSummary?.topic,
|
||||
newAlias = roomSummary?.canonicalAlias
|
||||
newCanonicalAlias = roomSummary?.canonicalAlias
|
||||
)
|
||||
}
|
||||
|
||||
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
|
||||
|
||||
powerLevelsContentLive.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
val permissions = RoomSettingsViewState.ActionPermissions(
|
||||
canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId),
|
||||
canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId),
|
||||
canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId),
|
||||
canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId)
|
||||
)
|
||||
setState { copy(actionPermissions = permissions) }
|
||||
}.disposeOnClear()
|
||||
powerLevelsContentLive
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
val permissions = RoomSettingsViewState.ActionPermissions(
|
||||
canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId),
|
||||
canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId),
|
||||
canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId),
|
||||
canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId)
|
||||
)
|
||||
setState { copy(actionPermissions = permissions) }
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
override fun handle(action: RoomSettingsAction) {
|
||||
when (action) {
|
||||
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
|
||||
is RoomSettingsAction.SetRoomName -> {
|
||||
setState { copy(newName = action.newName) }
|
||||
setState { copy(showSaveAction = shouldShowSaveAction(this)) }
|
||||
}
|
||||
is RoomSettingsAction.SetRoomTopic -> {
|
||||
setState { copy(newTopic = action.newTopic) }
|
||||
setState { copy(showSaveAction = shouldShowSaveAction(this)) }
|
||||
}
|
||||
is RoomSettingsAction.SetRoomHistoryVisibility -> {
|
||||
setState { copy(newHistoryVisibility = action.visibility) }
|
||||
setState { copy(showSaveAction = shouldShowSaveAction(this)) }
|
||||
}
|
||||
is RoomSettingsAction.SetRoomAlias -> {
|
||||
setState { copy(newAlias = action.alias) }
|
||||
setState { copy(showSaveAction = shouldShowSaveAction(this)) }
|
||||
}
|
||||
is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) }
|
||||
is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) }
|
||||
is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) }
|
||||
is RoomSettingsAction.SetRoomAlias -> setState { copy(newCanonicalAlias = action.alias) }
|
||||
is RoomSettingsAction.Save -> saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowSaveAction(state: RoomSettingsViewState): Boolean {
|
||||
val summary = state.roomSummary.invoke()
|
||||
return summary?.displayName != state.newName
|
||||
|| summary?.topic != state.newTopic
|
||||
|| summary?.canonicalAlias != state.newAlias
|
||||
|| state.newHistoryVisibility != null
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun saveSettings() = withState { state ->
|
||||
|
@ -130,13 +136,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
operationList.add(room.rx().updateTopic(state.newTopic ?: ""))
|
||||
}
|
||||
|
||||
if (state.newAlias != null && summary?.canonicalAlias != state.newAlias) {
|
||||
operationList.add(room.rx().addRoomAlias(state.newAlias))
|
||||
operationList.add(room.rx().updateCanonicalAlias(state.newAlias))
|
||||
if (state.newCanonicalAlias != null && summary?.canonicalAlias != state.newCanonicalAlias.takeIf { it.isNotEmpty() }) {
|
||||
operationList.add(room.rx().addRoomAlias(state.newCanonicalAlias))
|
||||
operationList.add(room.rx().updateCanonicalAlias(state.newCanonicalAlias))
|
||||
}
|
||||
|
||||
if (state.newHistoryVisibility != null) {
|
||||
operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility.name.toLowerCase(Locale.ROOT)))
|
||||
operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility))
|
||||
}
|
||||
|
||||
Observable
|
||||
|
@ -146,7 +152,6 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
{
|
||||
postLoading(false)
|
||||
setState { copy(newHistoryVisibility = null) }
|
||||
setState { copy(showSaveAction = false) }
|
||||
_viewEvents.post(RoomSettingsViewEvents.Success)
|
||||
},
|
||||
{
|
||||
|
|
|
@ -32,7 +32,7 @@ data class RoomSettingsViewState(
|
|||
val newName: String? = null,
|
||||
val newTopic: String? = null,
|
||||
val newHistoryVisibility: RoomHistoryVisibility? = null,
|
||||
val newAlias: String? = null,
|
||||
val newCanonicalAlias: String? = null,
|
||||
val showSaveAction: Boolean = false,
|
||||
val actionPermissions: ActionPermissions = ActionPermissions()
|
||||
) : MvRxState {
|
||||
|
|
|
@ -32,11 +32,11 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:background="?attr/colorAccent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/formTextInputTextInputLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/formTextInputTextInputLayout"
|
||||
android:background="?attr/colorAccent"
|
||||
tools:text="Add"/>
|
||||
tools:text="Add" />
|
||||
|
||||
<View
|
||||
android:id="@+id/formTextInputDivider"
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_attachment_type_selector"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/bg_attachment_type_selector"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
@ -28,9 +28,9 @@
|
|||
<ImageButton
|
||||
android:id="@+id/avatarCameraButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_camera_white_24dp"
|
||||
android:contentDescription="@string/attachment_type_camera"
|
||||
tools:background="@color/colorAccent" />
|
||||
android:src="@drawable/ic_attachment_camera_white_24dp"
|
||||
tools:background="@color/riotx_accent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
|
@ -50,9 +50,9 @@
|
|||
<ImageButton
|
||||
android:id="@+id/avatarGalleryButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_gallery_white_24dp"
|
||||
android:contentDescription="@string/attachment_type_gallery"
|
||||
tools:background="@color/colorAccent" />
|
||||
android:src="@drawable/ic_attachment_gallery_white_24dp"
|
||||
tools:background="@color/riotx_accent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
|
|
Loading…
Reference in a new issue