Merge pull request #1464 from vector-im/feature/room_settings
Room Settings: Name, Topic, Photo, Aliases, History Visibility
This commit is contained in:
commit
87a087c0b5
|
@ -9,6 +9,8 @@ Improvements 🙌:
|
|||
- "Add Matrix app" menu is now always visible (#1495)
|
||||
- Handle `/op`, `/deop`, and `/nick` commands (#12)
|
||||
- Prioritising Recovery key over Recovery passphrase (#1463)
|
||||
- Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455)
|
||||
- Update user avatar (#1054)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix dark theme issue on login screen (#1097)
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import android.net.Uri
|
||||
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.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
|
||||
|
@ -101,6 +103,30 @@ class RxRoom(private val room: Room) {
|
|||
fun invite(userId: String, reason: String? = null): Completable = completableBuilder<Unit> {
|
||||
room.invite(userId, reason, it)
|
||||
}
|
||||
|
||||
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||
room.updateTopic(topic, it)
|
||||
}
|
||||
|
||||
fun updateName(name: String): Completable = completableBuilder<Unit> {
|
||||
room.updateName(name, it)
|
||||
}
|
||||
|
||||
fun addRoomAlias(alias: String): Completable = completableBuilder<Unit> {
|
||||
room.addRoomAlias(alias, it)
|
||||
}
|
||||
|
||||
fun updateCanonicalAlias(alias: String): Completable = completableBuilder<Unit> {
|
||||
room.updateCanonicalAlias(alias, it)
|
||||
}
|
||||
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder<Unit> {
|
||||
room.updateHistoryReadability(readability, it)
|
||||
}
|
||||
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
||||
room.updateAvatar(avatarUri, fileName, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.profile
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
|
@ -48,6 +49,14 @@ interface ProfileService {
|
|||
*/
|
||||
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the avatar for this user
|
||||
* @param userId the userId to update the avatar of
|
||||
* @param newAvatarUri the new avatar uri of the user
|
||||
* @param fileName the fileName of selected image
|
||||
*/
|
||||
fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the current avatarUrl for this user.
|
||||
* @param userId the userId param to look for
|
||||
|
|
|
@ -27,7 +27,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||
*/
|
||||
data class RoomSummary constructor(
|
||||
val roomId: String,
|
||||
// Computed display name
|
||||
val displayName: String = "",
|
||||
val name: String = "",
|
||||
val topic: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val canonicalAlias: String? = null,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.room.powerlevels
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||
|
||||
/**
|
||||
|
@ -123,4 +124,59 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
else -> Role.Moderator.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room name
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room name
|
||||
*/
|
||||
fun isUserAbleToChangeRoomName(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_NAME] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room topic
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room topic
|
||||
*/
|
||||
fun isUserAbleToChangeRoomTopic(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_TOPIC] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room canonical alias
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room canonical alias
|
||||
*/
|
||||
fun isUserAbleToChangeRoomCanonicalAlias(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_CANONICAL_ALIAS] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room history readability
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room history readability
|
||||
*/
|
||||
fun isUserAbleToChangeRoomHistoryReadability(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_HISTORY_VISIBILITY] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room avatar
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room avatar
|
||||
*/
|
||||
fun isUserAbleToChangeRoomAvatar(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_AVATAR] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.room.state
|
||||
|
||||
import android.net.Uri
|
||||
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
|
||||
|
@ -31,6 +33,31 @@ interface StateService {
|
|||
*/
|
||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the name of the room
|
||||
*/
|
||||
fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Add new alias to the room.
|
||||
*/
|
||||
fun addRoomAlias(roomAlias: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the canonical alias of the room
|
||||
*/
|
||||
fun updateCanonicalAlias(alias: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the history readability of the room
|
||||
*/
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the avatar of the room
|
||||
*/
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
|
||||
|
|
|
@ -35,6 +35,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
|
|||
return RoomSummary(
|
||||
roomId = roomSummaryEntity.roomId,
|
||||
displayName = roomSummaryEntity.displayName ?: "",
|
||||
name = roomSummaryEntity.name ?: "",
|
||||
topic = roomSummaryEntity.topic ?: "",
|
||||
avatarUrl = roomSummaryEntity.avatarUrl ?: "",
|
||||
isDirect = roomSummaryEntity.isDirect,
|
||||
|
|
|
@ -28,6 +28,7 @@ internal open class RoomSummaryEntity(
|
|||
@PrimaryKey var roomId: String = "",
|
||||
var displayName: String? = "",
|
||||
var avatarUrl: String? = "",
|
||||
var name: String? = "",
|
||||
var topic: String? = "",
|
||||
var latestPreviewableEvent: TimelineEventEntity? = null,
|
||||
var heroes: RealmList<String> = RealmList(),
|
||||
|
|
|
@ -16,12 +16,16 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.content
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.internal.di.Authenticated
|
||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||
import im.vector.matrix.android.internal.network.awaitResponse
|
||||
import im.vector.matrix.android.internal.network.toFailure
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -31,12 +35,14 @@ import okhttp3.RequestBody.Companion.asRequestBody
|
|||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class FileUploader @Inject constructor(@Authenticated
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val eventBus: EventBus,
|
||||
private val context: Context,
|
||||
contentUrlResolver: ContentUrlResolver,
|
||||
moshi: Moshi) {
|
||||
|
||||
|
@ -59,6 +65,19 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||
return upload(uploadBody, filename, progressListener)
|
||||
}
|
||||
|
||||
suspend fun uploadFromUri(uri: Uri,
|
||||
filename: String?,
|
||||
mimeType: String?,
|
||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||
val inputStream = withContext(Dispatchers.IO) {
|
||||
context.contentResolver.openInputStream(uri)
|
||||
} ?: throw FileNotFoundException()
|
||||
|
||||
inputStream.use {
|
||||
return uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
|
||||
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.profile
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
|
@ -27,16 +28,22 @@ 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.session.content.FileUploader
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import io.realm.kotlin.where
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
|
||||
private val getProfileInfoTask: GetProfileInfoTask,
|
||||
private val setDisplayNameTask: SetDisplayNameTask) : ProfileService {
|
||||
private val setDisplayNameTask: SetDisplayNameTask,
|
||||
private val setAvatarUrlTask: SetAvatarUrlTask,
|
||||
private val fileUploader: FileUploader) : ProfileService {
|
||||
|
||||
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
val params = GetProfileInfoTask.Params(userId)
|
||||
|
@ -64,6 +71,17 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
|
||||
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg")
|
||||
setAvatarUrlTask
|
||||
.configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
val params = GetProfileInfoTask.Params(userId)
|
||||
return getProfileInfoTask
|
||||
|
|
|
@ -49,6 +49,12 @@ internal interface ProfileAPI {
|
|||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname")
|
||||
fun setDisplayName(@Path("userId") userId: String, @Body body: SetDisplayNameBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Change user avatar url.
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url")
|
||||
fun setAvatarUrl(@Path("userId") userId: String, @Body body: SetAvatarUrlBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Bind a threePid
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
|
||||
|
|
|
@ -54,4 +54,7 @@ internal abstract class ProfileModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SetAvatarUrlBody(
|
||||
/**
|
||||
* The new avatar url for this user.
|
||||
*/
|
||||
@Json(name = "avatar_url")
|
||||
val avatarUrl: String
|
||||
)
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.profile
|
||||
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class SetAvatarUrlTask : Task<SetAvatarUrlTask.Params, Unit> {
|
||||
data class Params(
|
||||
val userId: String,
|
||||
val newAvatarUrl: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSetAvatarUrlTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
private val eventBus: EventBus) : SetAvatarUrlTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
return executeRequest(eventBus) {
|
||||
val body = SetAvatarUrlBody(
|
||||
avatarUrl = params.newAvatarUrl
|
||||
)
|
||||
apiCall = profileAPI.setAvatarUrl(params.userId, body)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms
|
|||
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
|
||||
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
||||
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
|
||||
|
@ -311,6 +312,14 @@ internal interface RoomAPI {
|
|||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
||||
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
|
||||
|
||||
/**
|
||||
* Add alias to the room.
|
||||
* @param roomAlias the room alias.
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
|
||||
fun addRoomAlias(@Path("roomAlias") roomAlias: String,
|
||||
@Body body: AddRoomAliasBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Inform that the user is starting to type or has stopped typing
|
||||
*/
|
||||
|
|
|
@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
|||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.internal.session.DefaultFileService
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.alias.DefaultAddRoomAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||
|
@ -190,6 +192,9 @@ internal abstract class RoomModule {
|
|||
@Binds
|
||||
abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindAddRoomAliasTask(task: DefaultAddRoomAliasTask): AddRoomAliasTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.alias
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AddRoomAliasBody(
|
||||
/**
|
||||
* Required. The room id which the alias will be added to.
|
||||
*/
|
||||
@Json(name = "room_id") val roomId: String
|
||||
)
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.alias
|
||||
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface AddRoomAliasTask : Task<AddRoomAliasTask.Params, Unit> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val roomAlias: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultAddRoomAliasTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
private val eventBus: EventBus
|
||||
) : AddRoomAliasTask {
|
||||
|
||||
override suspend fun execute(params: AddRoomAliasTask.Params) {
|
||||
executeRequest<Unit>(eventBus) {
|
||||
apiCall = roomAPI.addRoomAlias(
|
||||
roomAlias = params.roomAlias,
|
||||
body = AddRoomAliasBody(
|
||||
roomId = params.roomId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.room.state
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
|
@ -23,17 +24,25 @@ 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
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.session.content.FileUploader
|
||||
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
|
||||
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val sendStateTask: SendStateTask
|
||||
private val sendStateTask: SendStateTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val fileUploader: FileUploader,
|
||||
private val addRoomAliasTask: AddRoomAliasTask
|
||||
) : StateService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -84,4 +93,51 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
|||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_NAME,
|
||||
body = mapOf("name" to name),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun addRoomAlias(roomAlias: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return addRoomAliasTask
|
||||
.configureWith(AddRoomAliasTask.Params(roomId, roomAlias)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun updateCanonicalAlias(alias: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
body = mapOf("alias" to alias),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
body = mapOf("history_visibility" to readability),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg")
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_AVATAR,
|
||||
body = mapOf("url" to response.contentUri),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
|||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
|
||||
|
@ -107,6 +108,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true,
|
||||
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
|
||||
|
||||
val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root
|
||||
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
|
||||
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
|
||||
val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
|
||||
|
@ -122,6 +124,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
|
||||
roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel<RoomNameContent>()?.name
|
||||
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
|
||||
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
|
||||
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
|
||||
|
|
|
@ -25,10 +25,12 @@ import androidx.core.view.isVisible
|
|||
import androidx.core.widget.ImageViewCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_profile_action)
|
||||
|
@ -51,6 +53,12 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
|
|||
@EpoxyAttribute
|
||||
var accessoryRes: Int = 0
|
||||
|
||||
@EpoxyAttribute
|
||||
var accessoryMatrixItem: MatrixItem? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var avatarRenderer: AvatarRenderer? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var editable: Boolean = true
|
||||
|
||||
|
@ -93,6 +101,13 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
|
|||
holder.secondaryAccessory.isVisible = false
|
||||
}
|
||||
|
||||
if (accessoryMatrixItem != null) {
|
||||
avatarRenderer?.render(accessoryMatrixItem!!, holder.secondaryAccessory)
|
||||
holder.secondaryAccessory.isVisible = true
|
||||
} else {
|
||||
holder.secondaryAccessory.isVisible = false
|
||||
}
|
||||
|
||||
if (editableRes != 0 && editable) {
|
||||
val tintColorSecondary = if (destructive) {
|
||||
tintColor
|
||||
|
|
|
@ -19,8 +19,10 @@ package im.vector.riotx.core.epoxy.profiles
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.riotx.core.epoxy.ClickListener
|
||||
import im.vector.riotx.core.epoxy.dividerItem
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
|
||||
fun EpoxyController.buildProfileSection(title: String) {
|
||||
profileSectionItem {
|
||||
|
@ -41,7 +43,9 @@ fun EpoxyController.buildProfileAction(
|
|||
destructive: Boolean = false,
|
||||
divider: Boolean = true,
|
||||
action: ClickListener? = null,
|
||||
@DrawableRes accessory: Int = 0
|
||||
@DrawableRes accessory: Int = 0,
|
||||
accessoryMatrixItem: MatrixItem? = null,
|
||||
avatarRenderer: AvatarRenderer? = null
|
||||
) {
|
||||
profileActionItem {
|
||||
iconRes(icon)
|
||||
|
@ -53,6 +57,8 @@ fun EpoxyController.buildProfileAction(
|
|||
destructive(destructive)
|
||||
title(title)
|
||||
accessoryRes(accessory)
|
||||
accessoryMatrixItem(accessoryMatrixItem)
|
||||
avatarRenderer(avatarRenderer)
|
||||
listener { _ ->
|
||||
action?.invoke()
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ package im.vector.riotx.features.attachments.preview
|
|||
import android.app.Activity.RESULT_CANCELED
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Menu
|
||||
|
@ -38,7 +37,6 @@ import com.airbnb.mvrx.args
|
|||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import com.yalantis.ucrop.UCropActivity
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.riotx.R
|
||||
|
@ -52,6 +50,7 @@ import im.vector.riotx.core.utils.SnapOnScrollListener
|
|||
import im.vector.riotx.core.utils.allGranted
|
||||
import im.vector.riotx.core.utils.attachSnapHelperWithListener
|
||||
import im.vector.riotx.core.utils.checkPermissions
|
||||
import im.vector.riotx.features.media.createUCropWithDefaultSettings
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_attachments_preview.*
|
||||
import timber.log.Timber
|
||||
|
@ -203,32 +202,7 @@ class AttachmentsPreviewFragment @Inject constructor(
|
|||
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
|
||||
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
|
||||
val uri = currentAttachment.queryUri
|
||||
UCrop.of(uri, destinationFile.toUri())
|
||||
.withOptions(
|
||||
UCrop.Options()
|
||||
.apply {
|
||||
setAllowedGestures(
|
||||
/* tabScale = */ UCropActivity.SCALE,
|
||||
/* tabRotate = */ UCropActivity.ALL,
|
||||
/* tabAspectRatio = */ UCropActivity.SCALE
|
||||
)
|
||||
setToolbarTitle(currentAttachment.name)
|
||||
// Disable freestyle crop, usability was not easy
|
||||
// setFreeStyleCropEnabled(true)
|
||||
// Color used for toolbar icon and text
|
||||
setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
|
||||
setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color))
|
||||
// Background
|
||||
setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
|
||||
// Status bar color (pb in dark mode, icon of the status bar are dark)
|
||||
setStatusBarColor(colorProvider.getColorFromAttribute(R.attr.riotx_header_panel_background))
|
||||
// Known issue: there is still orange color used by the lib
|
||||
// https://github.com/Yalantis/uCrop/issues/602
|
||||
setActiveControlsWidgetColor(colorProvider.getColor(R.color.riotx_accent))
|
||||
// Hide the logo (does not work)
|
||||
setLogoColor(Color.TRANSPARENT)
|
||||
}
|
||||
)
|
||||
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), currentAttachment.name)
|
||||
.start(requireContext(), this)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.form
|
||||
|
||||
import android.text.Editable
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatButton
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.platform.SimpleTextWatcher
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_form_text_input_with_button)
|
||||
abstract class FormEditTextWithButtonItem : VectorEpoxyModel<FormEditTextWithButtonItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var hint: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var value: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var enabled: Boolean = true
|
||||
|
||||
@EpoxyAttribute
|
||||
var buttonText: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var onTextChange: ((String) -> Unit)? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var onButtonClicked: ((View) -> Unit)? = null
|
||||
|
||||
private val onTextChangeListener = object : SimpleTextWatcher() {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
onTextChange?.invoke(s.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.textInputLayout.isEnabled = enabled
|
||||
holder.textInputLayout.hint = hint
|
||||
|
||||
// Update only if text is different
|
||||
if (holder.textInputEditText.text.toString() != value) {
|
||||
holder.textInputEditText.setText(value)
|
||||
}
|
||||
holder.textInputEditText.isEnabled = enabled
|
||||
|
||||
holder.textInputEditText.addTextChangedListener(onTextChangeListener)
|
||||
|
||||
holder.textInputButton.text = buttonText
|
||||
|
||||
holder.textInputButton.setOnClickListener(onButtonClicked)
|
||||
}
|
||||
|
||||
override fun shouldSaveViewState(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
super.unbind(holder)
|
||||
holder.textInputEditText.removeTextChangedListener(onTextChangeListener)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val textInputLayout by bind<TextInputLayout>(R.id.formTextInputTextInputLayout)
|
||||
val textInputEditText by bind<TextInputEditText>(R.id.formTextInputTextInputEditText)
|
||||
val textInputButton by bind<AppCompatButton>(R.id.formTextInputButton)
|
||||
}
|
||||
}
|
|
@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
|||
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomGuestAccessContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomJoinRules
|
||||
import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
|
||||
|
@ -47,6 +46,7 @@ import timber.log.Timber
|
|||
import javax.inject.Inject
|
||||
|
||||
class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
private val sp: StringProvider) {
|
||||
|
||||
private fun Event.isSentByCurrentUser() = senderId != null && senderId == sessionHolder.getSafeActiveSession()?.myUserId
|
||||
|
@ -223,12 +223,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
|
||||
|
||||
val formattedVisibility = when (historyVisibility) {
|
||||
RoomHistoryVisibility.SHARED -> sp.getString(R.string.notice_room_visibility_shared)
|
||||
RoomHistoryVisibility.INVITED -> sp.getString(R.string.notice_room_visibility_invited)
|
||||
RoomHistoryVisibility.JOINED -> sp.getString(R.string.notice_room_visibility_joined)
|
||||
RoomHistoryVisibility.WORLD_READABLE -> sp.getString(R.string.notice_room_visibility_world_readable)
|
||||
}
|
||||
val formattedVisibility = roomHistoryVisibilityFormatter.format(historyVisibility)
|
||||
return if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_made_future_room_visibility_by_you, formattedVisibility)
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.format
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomHistoryVisibilityFormatter @Inject constructor(
|
||||
private val stringProvider: StringProvider
|
||||
) {
|
||||
|
||||
fun format(roomHistoryVisibility: RoomHistoryVisibility): String {
|
||||
return when (roomHistoryVisibility) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,19 +16,41 @@
|
|||
|
||||
package im.vector.riotx.features.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
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
|
||||
import im.vector.riotx.core.utils.checkPermissions
|
||||
import im.vector.riotx.multipicker.MultiPicker
|
||||
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||
import kotlinx.android.synthetic.main.activity_big_image_viewer.*
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class BigImageViewerActivity : VectorBaseActivity() {
|
||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var colorProvider: ColorProvider
|
||||
@Inject lateinit var stringProvider: StringProvider
|
||||
|
||||
private var uri: Uri? = null
|
||||
|
||||
override fun getMenuRes() = R.menu.vector_big_avatar_viewer
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
|
@ -45,7 +67,7 @@ class BigImageViewerActivity : VectorBaseActivity() {
|
|||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
val uri = sessionHolder.getSafeActiveSession()
|
||||
uri = sessionHolder.getSafeActiveSession()
|
||||
?.contentUrlResolver()
|
||||
?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL))
|
||||
?.toUri()
|
||||
|
@ -57,14 +79,110 @@ class BigImageViewerActivity : VectorBaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
menu.findItem(R.id.bigAvatarEditAction).isVisible = shouldShowEditAction()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.bigAvatarEditAction) {
|
||||
showAvatarSelector()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun shouldShowEditAction(): Boolean {
|
||||
return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false)
|
||||
}
|
||||
|
||||
private fun showAvatarSelector() {
|
||||
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()
|
||||
}
|
||||
|
||||
private var avatarCameraUri: Uri? = null
|
||||
private fun onAvatarTypeSelected(isCamera: Boolean) {
|
||||
if (isCamera) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this)
|
||||
}
|
||||
} else {
|
||||
MultiPicker.get(MultiPicker.IMAGE).single().startWith(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRoomAvatarSelected(image: MultiPickerImageType) {
|
||||
val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
|
||||
val uri = image.contentUri
|
||||
createUCropWithDefaultSettings(this, uri, destinationFile.toUri(), image.displayName)
|
||||
.apply { withAspectRatio(1f, 1f) }
|
||||
.start(this)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
when (requestCode) {
|
||||
MultiPicker.REQUEST_CODE_TAKE_PHOTO -> {
|
||||
avatarCameraUri?.let { uri ->
|
||||
MultiPicker.get(MultiPicker.CAMERA)
|
||||
.getTakenPhoto(this, requestCode, resultCode, uri)
|
||||
?.let {
|
||||
onRoomAvatarSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
MultiPicker.REQUEST_CODE_PICK_IMAGE -> {
|
||||
MultiPicker
|
||||
.get(MultiPicker.IMAGE)
|
||||
.getSelectedFiles(this, requestCode, resultCode, data)
|
||||
.firstOrNull()?.let {
|
||||
// TODO. UCrop library cannot read from Gallery. For now, we will set avatar as it is.
|
||||
// onRoomAvatarSelected(it)
|
||||
onAvatarCropped(it.contentUri)
|
||||
}
|
||||
}
|
||||
UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (allGranted(grantResults)) {
|
||||
when (requestCode) {
|
||||
PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -> onAvatarTypeSelected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAvatarCropped(uri: Uri?) {
|
||||
if (uri != null) {
|
||||
setResult(Activity.RESULT_OK, Intent().setData(uri))
|
||||
this@BigImageViewerActivity.finish()
|
||||
} else {
|
||||
Toast.makeText(this, "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_TITLE = "EXTRA_TITLE"
|
||||
private const val EXTRA_IMAGE_URL = "EXTRA_IMAGE_URL"
|
||||
private const val EXTRA_CAN_EDIT_IMAGE = "EXTRA_CAN_EDIT_IMAGE"
|
||||
const val REQUEST_CODE = 1000
|
||||
|
||||
fun newIntent(context: Context, title: String?, imageUrl: String): Intent {
|
||||
fun newIntent(context: Context, title: String?, imageUrl: String, canEditImage: Boolean = false): Intent {
|
||||
return Intent(context, BigImageViewerActivity::class.java).apply {
|
||||
putExtra(EXTRA_TITLE, title)
|
||||
putExtra(EXTRA_IMAGE_URL, imageUrl)
|
||||
putExtra(EXTRA_CAN_EDIT_IMAGE, canEditImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.media
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import com.yalantis.ucrop.UCropActivity
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
|
||||
fun createUCropWithDefaultSettings(context: Context, source: Uri, destination: Uri, toolbarTitle: String?): UCrop {
|
||||
return UCrop.of(source, destination)
|
||||
.withOptions(
|
||||
UCrop.Options()
|
||||
.apply {
|
||||
setAllowedGestures(
|
||||
/* tabScale = */ UCropActivity.SCALE,
|
||||
/* tabRotate = */ UCropActivity.ALL,
|
||||
/* tabAspectRatio = */ UCropActivity.SCALE
|
||||
)
|
||||
setToolbarTitle(toolbarTitle)
|
||||
// Disable freestyle crop, usability was not easy
|
||||
// setFreeStyleCropEnabled(true)
|
||||
// Color used for toolbar icon and text
|
||||
setToolbarColor(ThemeUtils.getColor(context, R.attr.riotx_background))
|
||||
setToolbarWidgetColor(ThemeUtils.getColor(context, R.attr.vctr_toolbar_primary_text_color))
|
||||
// Background
|
||||
setRootViewBackgroundColor(ThemeUtils.getColor(context, R.attr.riotx_background))
|
||||
// Status bar color (pb in dark mode, icon of the status bar are dark)
|
||||
setStatusBarColor(ThemeUtils.getColor(context, R.attr.riotx_header_panel_background))
|
||||
// Known issue: there is still orange color used by the lib
|
||||
// https://github.com/Yalantis/uCrop/issues/602
|
||||
setActiveControlsWidgetColor(ContextCompat.getColor(context, R.color.riotx_accent))
|
||||
// Hide the logo (does not work)
|
||||
setLogoColor(Color.TRANSPARENT)
|
||||
}
|
||||
)
|
||||
}
|
|
@ -17,11 +17,13 @@
|
|||
|
||||
package im.vector.riotx.features.roomprofile
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class RoomProfileAction: VectorViewModelAction {
|
||||
object LeaveRoom: RoomProfileAction()
|
||||
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
|
||||
data class ChangeRoomAvatar(val uri: Uri, val fileName: String?) : RoomProfileAction()
|
||||
object ShareRoomProfile : RoomProfileAction()
|
||||
}
|
||||
|
|
|
@ -17,15 +17,23 @@
|
|||
|
||||
package im.vector.riotx.features.roomprofile
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
|
@ -36,7 +44,12 @@ 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.extensions.setTextOrHide
|
||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
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
|
||||
import im.vector.riotx.core.utils.checkPermissions
|
||||
import im.vector.riotx.core.utils.copyToClipboard
|
||||
import im.vector.riotx.core.utils.startSharePlainTextIntent
|
||||
import im.vector.riotx.features.crypto.util.toImageRes
|
||||
|
@ -45,10 +58,15 @@ import im.vector.riotx.features.home.room.list.actions.RoomListActionsArgs
|
|||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet
|
||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction
|
||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
||||
import im.vector.riotx.features.media.BigImageViewerActivity
|
||||
import im.vector.riotx.features.media.createUCropWithDefaultSettings
|
||||
import im.vector.riotx.multipicker.MultiPicker
|
||||
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_matrix_profile.*
|
||||
import kotlinx.android.synthetic.main.view_stub_room_profile_header.*
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
|
@ -96,6 +114,7 @@ class RoomProfileFragment @Inject constructor(
|
|||
is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
|
||||
is RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom()
|
||||
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
|
||||
RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
|
||||
}.exhaustive
|
||||
}
|
||||
roomListQuickActionsSharedActionViewModel
|
||||
|
@ -221,7 +240,89 @@ class RoomProfileFragment @Inject constructor(
|
|||
startSharePlainTextIntent(fragment = this, chooserTitle = null, text = permalink)
|
||||
}
|
||||
|
||||
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) {
|
||||
navigator.openBigImageViewer(requireActivity(), view, matrixItem)
|
||||
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) {
|
||||
if (matrixItem.avatarUrl?.isNotEmpty() == true) {
|
||||
val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!, it.canChangeAvatar)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, ViewCompat.getTransitionName(view) ?: "")
|
||||
startActivityForResult(intent, BigImageViewerActivity.REQUEST_CODE, options.toBundle())
|
||||
} else if (it.canChangeAvatar) {
|
||||
showAvatarSelector()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAvatarSelector() {
|
||||
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()
|
||||
}
|
||||
|
||||
private var avatarCameraUri: Uri? = null
|
||||
private fun onAvatarTypeSelected(isCamera: Boolean) {
|
||||
if (isCamera) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this)
|
||||
}
|
||||
} else {
|
||||
MultiPicker.get(MultiPicker.IMAGE).single().startWith(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRoomAvatarSelected(image: MultiPickerImageType) {
|
||||
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
|
||||
val uri = image.contentUri
|
||||
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
|
||||
.apply { withAspectRatio(1f, 1f) }
|
||||
.start(requireContext(), this)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
when (requestCode) {
|
||||
MultiPicker.REQUEST_CODE_TAKE_PHOTO -> {
|
||||
avatarCameraUri?.let { uri ->
|
||||
MultiPicker.get(MultiPicker.CAMERA)
|
||||
.getTakenPhoto(requireContext(), requestCode, resultCode, uri)
|
||||
?.let {
|
||||
onRoomAvatarSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
MultiPicker.REQUEST_CODE_PICK_IMAGE -> {
|
||||
MultiPicker
|
||||
.get(MultiPicker.IMAGE)
|
||||
.getSelectedFiles(requireContext(), requestCode, resultCode, data)
|
||||
.firstOrNull()?.let {
|
||||
// TODO. UCrop library cannot read from Gallery. For now, we will set avatar as it is.
|
||||
// onRoomAvatarSelected(it)
|
||||
onAvatarCropped(it.contentUri)
|
||||
}
|
||||
}
|
||||
UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
|
||||
BigImageViewerActivity.REQUEST_CODE -> data?.let { onAvatarCropped(it.data) }
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (allGranted(grantResults)) {
|
||||
when (requestCode) {
|
||||
PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -> onAvatarTypeSelected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAvatarCropped(uri: Uri?) {
|
||||
if (uri != null) {
|
||||
roomProfileViewModel.handle(RoomProfileAction.ChangeRoomAvatar(uri, getFilenameFromUri(context, uri)))
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,5 +26,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
|
|||
data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
|
||||
|
||||
object OnLeaveRoomSuccess : RoomProfileViewEvents()
|
||||
object OnChangeAvatarSuccess : RoomProfileViewEvents()
|
||||
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
|
||||
}
|
||||
|
|
|
@ -25,11 +25,14 @@ import com.squareup.inject.assisted.AssistedInject
|
|||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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.R
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
|
||||
import java.util.UUID
|
||||
|
||||
class RoomProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomProfileViewState,
|
||||
private val stringProvider: StringProvider,
|
||||
|
@ -62,12 +65,22 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
|
|||
.execute {
|
||||
copy(roomSummary = it)
|
||||
}
|
||||
|
||||
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
|
||||
|
||||
powerLevelsContentLive
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) }
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
override fun handle(action: RoomProfileAction) = when (action) {
|
||||
RoomProfileAction.LeaveRoom -> handleLeaveRoom()
|
||||
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
|
||||
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
|
||||
is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action)
|
||||
}
|
||||
|
||||
private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) {
|
||||
|
@ -96,4 +109,18 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
|
|||
_viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
)
|
||||
.disposeOnClear()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,8 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
|
|||
|
||||
data class RoomProfileViewState(
|
||||
val roomId: String,
|
||||
val roomSummary: Async<RoomSummary> = Uninitialized
|
||||
val roomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val canChangeAvatar: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
|
||||
|
|
|
@ -16,11 +16,14 @@
|
|||
|
||||
package im.vector.riotx.features.roomprofile.settings
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class RoomSettingsAction : VectorViewModelAction {
|
||||
data class SetRoomName(val newName: String) : RoomSettingsAction()
|
||||
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
|
||||
data class SetRoomAvatar(val newAvatarUrl: String) : RoomSettingsAction()
|
||||
data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction()
|
||||
data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction()
|
||||
object EnableEncryption : RoomSettingsAction()
|
||||
object Save : RoomSettingsAction()
|
||||
}
|
||||
|
|
|
@ -17,21 +17,30 @@
|
|||
package im.vector.riotx.features.roomprofile.settings
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.profiles.buildProfileAction
|
||||
import im.vector.riotx.core.epoxy.profiles.buildProfileSection
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.form.formEditTextItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Add other feature here (waiting for design)
|
||||
class RoomSettingsController @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
colorProvider: ColorProvider
|
||||
) : TypedEpoxyController<RoomSettingsViewState>() {
|
||||
|
||||
interface Callback {
|
||||
fun onEnableEncryptionClicked()
|
||||
fun onNameChanged(name: String)
|
||||
fun onTopicChanged(topic: String)
|
||||
fun onHistoryVisibilityClicked()
|
||||
fun onAliasChanged(alias: String)
|
||||
}
|
||||
|
||||
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
|
||||
|
@ -45,10 +54,56 @@ class RoomSettingsController @Inject constructor(
|
|||
override fun buildModels(data: RoomSettingsViewState?) {
|
||||
val roomSummary = data?.roomSummary?.invoke() ?: return
|
||||
|
||||
val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: ""
|
||||
val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) }
|
||||
|
||||
buildProfileSection(
|
||||
stringProvider.getString(R.string.settings)
|
||||
)
|
||||
|
||||
formEditTextItem {
|
||||
id("name")
|
||||
enabled(data.actionPermissions.canChangeName)
|
||||
value(data.newName ?: roomSummary.displayName)
|
||||
hint(stringProvider.getString(R.string.room_settings_name_hint))
|
||||
|
||||
onTextChange { text ->
|
||||
callback?.onNameChanged(text)
|
||||
}
|
||||
}
|
||||
|
||||
formEditTextItem {
|
||||
id("topic")
|
||||
enabled(data.actionPermissions.canChangeTopic)
|
||||
value(data.newTopic ?: roomSummary.topic)
|
||||
hint(stringProvider.getString(R.string.room_settings_topic_hint))
|
||||
|
||||
onTextChange { text ->
|
||||
callback?.onTopicChanged(text)
|
||||
}
|
||||
}
|
||||
|
||||
formEditTextItem {
|
||||
id("alias")
|
||||
enabled(data.actionPermissions.canChangeCanonicalAlias)
|
||||
value(data.newCanonicalAlias ?: roomSummary.canonicalAlias)
|
||||
hint(stringProvider.getString(R.string.room_settings_addresses_add_new_address))
|
||||
|
||||
onTextChange { text ->
|
||||
callback?.onAliasChanged(text)
|
||||
}
|
||||
}
|
||||
|
||||
buildProfileAction(
|
||||
id = "historyReadability",
|
||||
title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title),
|
||||
subtitle = newHistoryVisibility ?: historyVisibility,
|
||||
dividerColor = dividerColor,
|
||||
divider = false,
|
||||
editable = data.actionPermissions.canChangeHistoryReadability,
|
||||
action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() }
|
||||
)
|
||||
|
||||
if (roomSummary.isEncrypted) {
|
||||
buildProfileAction(
|
||||
id = "encryption",
|
||||
|
@ -69,4 +124,9 @@ class RoomSettingsController @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomHistoryVisibilityEvent(event: Event): String? {
|
||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
|
||||
return roomHistoryVisibilityFormatter.format(historyVisibility)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,19 +17,26 @@
|
|||
package im.vector.riotx.features.roomprofile.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.riotx.R
|
||||
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.utils.toast
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
|
||||
import im.vector.riotx.features.roomprofile.RoomProfileArgs
|
||||
import kotlinx.android.synthetic.main.fragment_room_setting_generic.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
|
@ -38,6 +45,7 @@ import javax.inject.Inject
|
|||
class RoomSettingsFragment @Inject constructor(
|
||||
val viewModelFactory: RoomSettingsViewModel.Factory,
|
||||
private val controller: RoomSettingsController,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
private val avatarRenderer: AvatarRenderer
|
||||
) : VectorBaseFragment(), RoomSettingsController.Callback {
|
||||
|
||||
|
@ -46,6 +54,8 @@ class RoomSettingsFragment @Inject constructor(
|
|||
|
||||
override fun getLayoutResId() = R.layout.fragment_room_setting_generic
|
||||
|
||||
override fun getMenuRes() = R.menu.vector_room_settings
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
controller.callback = this
|
||||
|
@ -57,20 +67,50 @@ class RoomSettingsFragment @Inject constructor(
|
|||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is RoomSettingsViewEvents.Failure -> showFailure(it.throwable)
|
||||
is RoomSettingsViewEvents.Success -> showSuccess()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSuccess() {
|
||||
activity?.toast(R.string.room_settings_save_success)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
recyclerView.cleanup()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
withState(viewModel) { state ->
|
||||
menu.findItem(R.id.roomSettingsSaveAction).isVisible = state.showSaveAction
|
||||
}
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.roomSettingsSaveAction) {
|
||||
viewModel.handle(RoomSettingsAction.Save)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { viewState ->
|
||||
controller.setData(viewState)
|
||||
renderRoomSummary(viewState)
|
||||
}
|
||||
|
||||
private fun renderRoomSummary(state: RoomSettingsViewState) {
|
||||
waiting_view.isVisible = state.isLoading
|
||||
|
||||
state.roomSummary()?.let {
|
||||
roomSettingsToolbarTitleView.text = it.displayName
|
||||
avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView)
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onEnableEncryptionClicked() {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.room_settings_enable_encryption_dialog_title)
|
||||
|
@ -82,12 +122,43 @@ class RoomSettingsFragment @Inject constructor(
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun renderRoomSummary(state: RoomSettingsViewState) {
|
||||
waiting_view.isVisible = state.isLoading
|
||||
override fun onNameChanged(name: String) {
|
||||
viewModel.handle(RoomSettingsAction.SetRoomName(name))
|
||||
}
|
||||
|
||||
state.roomSummary()?.let {
|
||||
roomSettingsToolbarTitleView.text = it.displayName
|
||||
avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView)
|
||||
override fun onTopicChanged(topic: String) {
|
||||
viewModel.handle(RoomSettingsAction.SetRoomTopic(topic))
|
||||
}
|
||||
|
||||
override fun onHistoryVisibilityClicked() = withState(viewModel) { state ->
|
||||
val historyVisibilities = arrayOf(
|
||||
RoomHistoryVisibility.SHARED,
|
||||
RoomHistoryVisibility.INVITED,
|
||||
RoomHistoryVisibility.JOINED,
|
||||
RoomHistoryVisibility.WORLD_READABLE
|
||||
)
|
||||
val currentHistoryVisibility =
|
||||
state.newHistoryVisibility ?: state.historyVisibilityEvent?.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
||||
val currentHistoryVisibilityIndex = historyVisibilities.indexOf(currentHistoryVisibility)
|
||||
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.room_settings_room_read_history_rules_pref_title)
|
||||
setSingleChoiceItems(
|
||||
historyVisibilities
|
||||
.map { roomHistoryVisibilityFormatter.format(it) }
|
||||
.toTypedArray(),
|
||||
currentHistoryVisibilityIndex) { dialog, which ->
|
||||
if (which != currentHistoryVisibilityIndex) {
|
||||
viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(historyVisibilities[which]))
|
||||
}
|
||||
dialog.cancel()
|
||||
}
|
||||
show()
|
||||
}
|
||||
return@withState
|
||||
}
|
||||
|
||||
override fun onAliasChanged(alias: String) {
|
||||
viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
|
|||
*/
|
||||
sealed class RoomSettingsViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : RoomSettingsViewEvents()
|
||||
object Success : RoomSettingsViewEvents()
|
||||
}
|
||||
|
|
|
@ -23,9 +23,15 @@ import com.squareup.inject.assisted.Assisted
|
|||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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
|
||||
|
||||
class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState,
|
||||
private val session: Session)
|
||||
|
@ -49,41 +55,130 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
|
||||
init {
|
||||
observeRoomSummary()
|
||||
observeState()
|
||||
}
|
||||
|
||||
private fun observeState() {
|
||||
selectSubscribe(
|
||||
RoomSettingsViewState::newName,
|
||||
RoomSettingsViewState::newCanonicalAlias,
|
||||
RoomSettingsViewState::newTopic,
|
||||
RoomSettingsViewState::newHistoryVisibility,
|
||||
RoomSettingsViewState::roomSummary) { newName,
|
||||
newCanonicalAlias,
|
||||
newTopic,
|
||||
newHistoryVisibility,
|
||||
asyncSummary ->
|
||||
val summary = asyncSummary()
|
||||
setState {
|
||||
copy(
|
||||
showSaveAction = summary?.name != newName
|
||||
|| summary?.topic != newTopic
|
||||
|| summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() }
|
||||
|| newHistoryVisibility != null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeRoomSummary() {
|
||||
room.rx().liveRoomSummary()
|
||||
.unwrap()
|
||||
.execute { async ->
|
||||
copy(roomSummary = async)
|
||||
val roomSummary = async.invoke()
|
||||
copy(
|
||||
historyVisibilityEvent = room.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY),
|
||||
roomSummary = async,
|
||||
newName = roomSummary?.name,
|
||||
newTopic = roomSummary?.topic,
|
||||
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()
|
||||
}
|
||||
|
||||
override fun handle(action: RoomSettingsAction) {
|
||||
when (action) {
|
||||
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
|
||||
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
|
||||
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.SetRoomCanonicalAlias -> setState { copy(newCanonicalAlias = action.newCanonicalAlias) }
|
||||
is RoomSettingsAction.Save -> saveSettings()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun saveSettings() = withState { state ->
|
||||
postLoading(true)
|
||||
|
||||
val operationList = mutableListOf<Completable>()
|
||||
|
||||
val summary = state.roomSummary.invoke()
|
||||
|
||||
if (summary?.name != state.newName) {
|
||||
operationList.add(room.rx().updateName(state.newName ?: ""))
|
||||
}
|
||||
if (summary?.topic != state.newTopic) {
|
||||
operationList.add(room.rx().updateTopic(state.newTopic ?: ""))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
Observable
|
||||
.fromIterable(operationList)
|
||||
.concatMapCompletable { it }
|
||||
.subscribe(
|
||||
{
|
||||
postLoading(false)
|
||||
setState { copy(newHistoryVisibility = null) }
|
||||
_viewEvents.post(RoomSettingsViewEvents.Success)
|
||||
},
|
||||
{
|
||||
postLoading(false)
|
||||
_viewEvents.post(RoomSettingsViewEvents.Failure(it))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleEnableEncryption() {
|
||||
setState {
|
||||
copy(isLoading = true)
|
||||
}
|
||||
postLoading(true)
|
||||
|
||||
room.enableEncryption(callback = object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
setState {
|
||||
copy(isLoading = false)
|
||||
}
|
||||
|
||||
postLoading(false)
|
||||
_viewEvents.post(RoomSettingsViewEvents.Failure(failure))
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
setState {
|
||||
copy(isLoading = false)
|
||||
}
|
||||
postLoading(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun postLoading(isLoading: Boolean) {
|
||||
setState {
|
||||
copy(isLoading = isLoading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,30 @@ package im.vector.riotx.features.roomprofile.settings
|
|||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
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.session.room.model.RoomSummary
|
||||
import im.vector.riotx.features.roomprofile.RoomProfileArgs
|
||||
|
||||
data class RoomSettingsViewState(
|
||||
val roomId: String,
|
||||
val historyVisibilityEvent: Event? = null,
|
||||
val roomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val isLoading: Boolean = false
|
||||
val isLoading: Boolean = false,
|
||||
val newName: String? = null,
|
||||
val newTopic: String? = null,
|
||||
val newHistoryVisibility: RoomHistoryVisibility? = null,
|
||||
val newCanonicalAlias: String? = null,
|
||||
val showSaveAction: Boolean = false,
|
||||
val actionPermissions: ActionPermissions = ActionPermissions()
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
|
||||
|
||||
data class ActionPermissions(
|
||||
val canChangeName: Boolean = false,
|
||||
val canChangeTopic: Boolean = false,
|
||||
val canChangeCanonicalAlias: Boolean = false,
|
||||
val canChangeHistoryReadability: Boolean = false
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,13 +20,16 @@ package im.vector.riotx.features.settings
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.text.Editable
|
||||
import android.util.Patterns
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
|
@ -36,6 +39,7 @@ import com.bumptech.glide.Glide
|
|||
import com.bumptech.glide.load.engine.cache.DiskCache
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.failure.isInvalidPassword
|
||||
|
@ -44,25 +48,32 @@ import im.vector.matrix.android.api.session.integrationmanager.IntegrationManage
|
|||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||
import im.vector.riotx.core.platform.SimpleTextWatcher
|
||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
import im.vector.riotx.core.preference.VectorPreference
|
||||
import im.vector.riotx.core.preference.VectorSwitchPreference
|
||||
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.TextUtils
|
||||
import im.vector.riotx.core.utils.allGranted
|
||||
import im.vector.riotx.core.utils.checkPermissions
|
||||
import im.vector.riotx.core.utils.copyToClipboard
|
||||
import im.vector.riotx.core.utils.getSizeOfFiles
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.MainActivityArgs
|
||||
import im.vector.riotx.features.media.createUCropWithDefaultSettings
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import im.vector.riotx.features.workers.signout.SignOutUiWorker
|
||||
import im.vector.riotx.multipicker.MultiPicker
|
||||
import im.vector.riotx.multipicker.entity.MultiPickerImageType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||
|
||||
|
@ -72,6 +83,8 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
private var mDisplayedEmails = ArrayList<String>()
|
||||
private var mDisplayedPhoneNumber = ArrayList<String>()
|
||||
|
||||
private var avatarCameraUri: Uri? = null
|
||||
|
||||
private val mUserSettingsCategory by lazy {
|
||||
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!!
|
||||
}
|
||||
|
@ -281,7 +294,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
if (allGranted(grantResults)) {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
|
||||
changeAvatar()
|
||||
onAvatarTypeSelected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -291,8 +304,27 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
when (requestCode) {
|
||||
REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList()
|
||||
REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data)
|
||||
REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList()
|
||||
REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data)
|
||||
MultiPicker.REQUEST_CODE_TAKE_PHOTO -> {
|
||||
avatarCameraUri?.let { uri ->
|
||||
MultiPicker.get(MultiPicker.CAMERA)
|
||||
.getTakenPhoto(requireContext(), requestCode, resultCode, uri)
|
||||
?.let {
|
||||
onAvatarSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
MultiPicker.REQUEST_CODE_PICK_IMAGE -> {
|
||||
MultiPicker
|
||||
.get(MultiPicker.IMAGE)
|
||||
.getSelectedFiles(requireContext(), requestCode, resultCode, data)
|
||||
.firstOrNull()?.let {
|
||||
// TODO. UCrop library cannot read from Gallery. For now, we will set avatar as it is.
|
||||
onAvatarCropped(it.contentUri)
|
||||
}
|
||||
}
|
||||
UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
|
||||
/* TODO
|
||||
VectorUtils.TAKE_IMAGE -> {
|
||||
val thumbnailUri = VectorUtils.getThumbnailUriFromIntent(activity, data, session.mediaCache)
|
||||
|
@ -370,21 +402,59 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
* Update the avatar.
|
||||
*/
|
||||
private fun onUpdateAvatarClick() {
|
||||
notImplemented()
|
||||
|
||||
/* TODO
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
changeAvatar()
|
||||
}
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
private fun changeAvatar() {
|
||||
/* TODO
|
||||
val intent = Intent(activity, VectorMediaPickerActivity::class.java)
|
||||
intent.putExtra(VectorMediaPickerActivity.EXTRA_AVATAR_MODE, true)
|
||||
startActivityForResult(intent, VectorUtils.TAKE_IMAGE)
|
||||
*/
|
||||
private fun onAvatarTypeSelected(isCamera: Boolean) {
|
||||
if (isCamera) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this)
|
||||
}
|
||||
} else {
|
||||
MultiPicker.get(MultiPicker.IMAGE).single().startWith(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAvatarSelected(image: MultiPickerImageType) {
|
||||
val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
|
||||
val uri = image.contentUri
|
||||
createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
|
||||
.apply { withAspectRatio(1f, 1f) }
|
||||
.start(requireContext(), this)
|
||||
}
|
||||
|
||||
private fun onAvatarCropped(uri: Uri?) {
|
||||
if (uri != null) {
|
||||
uploadAvatar(uri)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadAvatar(uri: Uri) {
|
||||
displayLoadingView()
|
||||
|
||||
session.updateAvatar(session.myUserId, uri, getFilenameFromUri(context, uri) ?: UUID.randomUUID().toString(), object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
if (!isAdded) return
|
||||
|
||||
mUserAvatarPreference.refreshAvatar()
|
||||
onCommonDone(null)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (!isAdded) return
|
||||
onCommonDone(failure.localizedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==============================================================================================================
|
||||
|
@ -505,9 +575,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
} */
|
||||
}
|
||||
|
||||
// ==============================================================================================================
|
||||
// Email management
|
||||
// ==============================================================================================================
|
||||
// ==============================================================================================================
|
||||
// Email management
|
||||
// ==============================================================================================================
|
||||
|
||||
/**
|
||||
* Refresh the emails list
|
||||
|
@ -632,47 +702,47 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
|||
*
|
||||
* @param pid the used pid.
|
||||
*/
|
||||
/* TODO
|
||||
private fun showEmailValidationDialog(pid: ThreePid) {
|
||||
activity?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.account_email_validation_title)
|
||||
.setMessage(R.string.account_email_validation_message)
|
||||
.setPositiveButton(R.string._continue) { _, _ ->
|
||||
session.myUser.add3Pid(pid, true, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(info: Void?) {
|
||||
/* TODO
|
||||
private fun showEmailValidationDialog(pid: ThreePid) {
|
||||
activity?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.account_email_validation_title)
|
||||
.setMessage(R.string.account_email_validation_message)
|
||||
.setPositiveButton(R.string._continue) { _, _ ->
|
||||
session.myUser.add3Pid(pid, true, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(info: Void?) {
|
||||
it.runOnUiThread {
|
||||
hideLoadingView()
|
||||
refreshEmailsList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) {
|
||||
it.runOnUiThread {
|
||||
hideLoadingView()
|
||||
refreshEmailsList()
|
||||
it.toast(R.string.account_email_validation_error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNetworkError(e: Exception) {
|
||||
} else {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMatrixError(e: MatrixError) {
|
||||
if (TextUtils.equals(e.errcode, MatrixError.THREEPID_AUTH_FAILED)) {
|
||||
it.runOnUiThread {
|
||||
hideLoadingView()
|
||||
it.toast(R.string.account_email_validation_error)
|
||||
}
|
||||
} else {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
hideLoadingView()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
} */
|
||||
override fun onUnexpectedError(e: Exception) {
|
||||
onCommonDone(e.localizedMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
hideLoadingView()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Display a dialog which asks confirmation for the deletion of a 3pid
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?riotx_background"
|
||||
android:minHeight="@dimen/item_form_min_height">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/formTextInputTextInputLayout"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
app:layout_constraintBottom_toTopOf="@+id/formTextInputDivider"
|
||||
app:layout_constraintEnd_toStartOf="@id/formTextInputButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/formTextInputTextInputEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:hint="@string/create_room_name_hint" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/formTextInputButton"
|
||||
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"
|
||||
tools:text="Add" />
|
||||
|
||||
<View
|
||||
android:id="@+id/formTextInputDivider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="?riotx_header_panel_border_mobile"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/bg_attachment_type_selector"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/avatarCameraButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:contentDescription="@string/attachment_type_camera"
|
||||
android:src="@drawable/ic_attachment_camera_white_24dp"
|
||||
tools:background="@color/riotx_accent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:importantForAccessibility="no"
|
||||
android:text="@string/attachment_type_camera" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/avatarGalleryButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:contentDescription="@string/attachment_type_gallery"
|
||||
android:src="@drawable/ic_attachment_gallery_white_24dp"
|
||||
tools:background="@color/riotx_accent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:importantForAccessibility="no"
|
||||
android:text="@string/attachment_type_gallery" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/bigAvatarEditAction"
|
||||
android:title="@string/edit"
|
||||
android:icon="@drawable/ic_edit"
|
||||
app:iconTint="?attr/colorAccent"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/roomSettingsSaveAction"
|
||||
android:title="@string/save"
|
||||
app:iconTint="?attr/colorAccent"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -2496,4 +2496,9 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
|||
<string name="save_your_security_key_title">Save your Security Key</string>
|
||||
<string name="save_your_security_key_notice">Store your Security Key somewhere safe, like a password manager or a safe.</string>
|
||||
|
||||
</resources>
|
||||
<!-- Room Settings -->
|
||||
<string name="room_settings_name_hint">Room Name</string>
|
||||
<string name="room_settings_topic_hint">Topic</string>
|
||||
<string name="room_settings_save_success">You changed room settings successfully</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue