diff --git a/CHANGES.md b/CHANGES.md index 3f2657a65cb..06c14244040 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 5cc9d1fc00b..b91949778db 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -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 { room.invite(userId, reason, it) } + + fun updateTopic(topic: String): Completable = completableBuilder { + room.updateTopic(topic, it) + } + + fun updateName(name: String): Completable = completableBuilder { + room.updateName(name, it) + } + + fun addRoomAlias(alias: String): Completable = completableBuilder { + room.addRoomAlias(alias, it) + } + + fun updateCanonicalAlias(alias: String): Completable = completableBuilder { + room.updateCanonicalAlias(alias, it) + } + + fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder { + room.updateHistoryReadability(readability, it) + } + + fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { + room.updateAvatar(avatarUri, fileName, it) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt index 3d084336e3e..d7569bbc18b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt @@ -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): 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): Cancelable + /** * Return the current avatarUrl for this user. * @param userId the userId param to look for diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 6f21b9eeae0..3cb118f0ecc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt index f434859f6ea..6361a46bac7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -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 + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 827ce50e13e..9c70baefd34 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -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): Cancelable + /** + * Update the name of the room + */ + fun updateName(name: String, callback: MatrixCallback): Cancelable + + /** + * Add new alias to the room. + */ + fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable + + /** + * Update the canonical alias of the room + */ + fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable + + /** + * Update the history readability of the room + */ + fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable + + /** + * Update the avatar of the room + */ + fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable + fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index d7bc0a17ae4..0754ecb7d9c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 521ab85a754..acfd484debe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -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 = RealmList(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 1153b39b0a1..6ee508b02a9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt index 459d53607b0..06127b6c6d4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -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>): 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): 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>): Cancelable { val params = GetProfileInfoTask.Params(userId) return getProfileInfoTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt index b3b726a3155..7dc47634038 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt @@ -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 + /** + * 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 + /** * Bind a threePid * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt index d83c305c10a..b86d0ee07a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt @@ -54,4 +54,7 @@ internal abstract class ProfileModule { @Binds abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask + + @Binds + abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlBody.kt new file mode 100644 index 00000000000..0288853e28d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlTask.kt new file mode 100644 index 00000000000..263922ca78a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/SetAvatarUrlTask.kt @@ -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 { + 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) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index e01e58856f1..59fc0efbc0a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -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 + /** + * 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 + /** * Inform that the user is starting to type or has stopped typing */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 0572a375060..5e84920fbd4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasBody.kt new file mode 100644 index 00000000000..0150c277ea7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasBody.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasTask.kt new file mode 100644 index 00000000000..baad3a3cde3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/AddRoomAliasTask.kt @@ -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 { + 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(eventBus) { + apiCall = roomAPI.addRoomAlias( + roomAlias = params.roomAlias, + body = AddRoomAliasBody( + roomId = params.roomId + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 6646f08c2dc..198b601ea5d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -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): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_NAME, + body = mapOf("name" to name), + callback = callback, + stateKey = null + ) + } + + override fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable { + return addRoomAliasTask + .configureWith(AddRoomAliasTask.Params(roomId, roomAlias)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun updateCanonicalAlias(alias: String, callback: MatrixCallback): 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): 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): 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 + ) + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt index 82b2cf36586..fad9938387f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryUpdater.kt @@ -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()?.name roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel() diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt index a015358d8bb..80b78d0d701 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt @@ -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() @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() 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 diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt index 693efc54188..1f3aff0de6d 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index 3b1972ffbc1..59629743b59 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/features/form/FormEditTextWithButtonItem.kt b/vector/src/main/java/im/vector/riotx/features/form/FormEditTextWithButtonItem.kt new file mode 100644 index 00000000000..0650c0f55c9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/form/FormEditTextWithButtonItem.kt @@ -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() { + + @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(R.id.formTextInputTextInputLayout) + val textInputEditText by bind(R.id.formTextInputTextInputEditText) + val textInputButton by bind(R.id.formTextInputButton) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 37debace899..89e170e25e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -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()?.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 { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt new file mode 100644 index 00000000000..c5de02f57f7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt index 24b595d1824..e13bcf9dde6 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/BigImageViewerActivity.kt @@ -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, 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) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/UCropHelper.kt b/vector/src/main/java/im/vector/riotx/features/media/UCropHelper.kt new file mode 100644 index 00000000000..5e3ca5f4833 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/UCropHelper.kt @@ -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) + } + ) +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt index 545d67c314e..20498dbf842 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index 52f2d95c934..f0cb29ea6b6 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -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, 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() + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt index 7a08a08126d..78df127f724 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt index d49727d12d0..373dd6b56c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt @@ -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() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt index aed1488b073..76cc3f1f072 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt @@ -24,7 +24,8 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary data class RoomProfileViewState( val roomId: String, - val roomSummary: Async = Uninitialized + val roomSummary: Async = Uninitialized, + val canChangeAvatar: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt index 3c1b10cf8ec..908b72606d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index fdd127e68b3..94177159f0e 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -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() { 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()?.historyVisibility ?: return null + return roomHistoryVisibilityFormatter.format(historyVisibility) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt index d2e06f30e70..840ec9f00cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsFragment.kt @@ -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()?.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)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt index 4856a935a8d..c30a5ff9c94 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index f1dee870056..e198375cfb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -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() + + 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 { 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) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index d68ed6853eb..a86fbf8cfaa 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -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 = 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 + ) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 5ff521400f4..17739c2503b 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -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() private var mDisplayedPhoneNumber = ArrayList() + private var avatarCameraUri: Uri? = null + private val mUserSettingsCategory by lazy { findPreference(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!! } @@ -281,7 +294,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, 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() + 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() + } - /* TODO - if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { - changeAvatar() + 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 changeAvatar() { - /* TODO - val intent = Intent(activity, VectorMediaPickerActivity::class.java) - intent.putExtra(VectorMediaPickerActivity.EXTRA_AVATAR_MODE, true) - startActivityForResult(intent, VectorUtils.TAKE_IMAGE) - */ + 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 { + 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 { - override fun onSuccess(info: Void?) { - it.runOnUiThread { - hideLoadingView() - refreshEmailsList() - } +/* 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 { + override fun onSuccess(info: Void?) { + it.runOnUiThread { + hideLoadingView() + refreshEmailsList() } + } - override fun onNetworkError(e: Exception) { - onCommonDone(e.localizedMessage) - } + 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() - it.toast(R.string.account_email_validation_error) - } - } 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) } - } - - override fun onUnexpectedError(e: Exception) { + } else { 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 diff --git a/vector/src/main/res/layout/item_form_text_input_with_button.xml b/vector/src/main/res/layout/item_form_text_input_with_button.xml new file mode 100644 index 00000000000..8a7e60c0060 --- /dev/null +++ b/vector/src/main/res/layout/item_form_text_input_with_button.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_avatar_selector.xml b/vector/src/main/res/layout/view_avatar_selector.xml new file mode 100644 index 00000000000..c303347960e --- /dev/null +++ b/vector/src/main/res/layout/view_avatar_selector.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/menu/vector_big_avatar_viewer.xml b/vector/src/main/res/menu/vector_big_avatar_viewer.xml new file mode 100644 index 00000000000..aff0ac0e034 --- /dev/null +++ b/vector/src/main/res/menu/vector_big_avatar_viewer.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_room_settings.xml b/vector/src/main/res/menu/vector_room_settings.xml new file mode 100644 index 00000000000..f8a569f0625 --- /dev/null +++ b/vector/src/main/res/menu/vector_room_settings.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 5ac50b24840..3b3af55d1f4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2496,4 +2496,9 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Save your Security Key Store your Security Key somewhere safe, like a password manager or a safe. - \ No newline at end of file + + Room Name + Topic + You changed room settings successfully + +