RoomSettingsViewModel.kt 13 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
 * Copyright 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.
 */

17
package im.vector.app.features.roomprofile.settings
18

19
import androidx.core.net.toFile
Valere's avatar
Valere committed
20
import com.airbnb.mvrx.ActivityViewModelContext
21
22
23
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
24
25
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
Valere's avatar
Valere committed
26
import dagger.assisted.AssistedInject
Benoit Marty's avatar
Benoit Marty committed
27
28
29
30
31
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import io.reactivex.Completable
import io.reactivex.Observable
32
import org.matrix.android.sdk.api.extensions.tryOrNull
33
import org.matrix.android.sdk.api.query.QueryStringValue
Benoît Marty's avatar
Benoît Marty committed
34
35
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
36
37
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
38
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
39
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
40
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
Benoît Marty's avatar
Benoît Marty committed
41
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
42
import org.matrix.android.sdk.rx.mapOptional
Benoît Marty's avatar
Benoît Marty committed
43
44
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
45
46
47

class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState,
                                                        private val session: Session)
48
    : VectorViewModel<RoomSettingsViewState, RoomSettingsAction, RoomSettingsViewEvents>(initialState) {
49

50
    @AssistedFactory
51
52
53
54
55
56
57
58
    interface Factory {
        fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel
    }

    companion object : MvRxViewModelFactory<RoomSettingsViewModel, RoomSettingsViewState> {

        @JvmStatic
        override fun create(viewModelContext: ViewModelContext, state: RoomSettingsViewState): RoomSettingsViewModel? {
Valere's avatar
Valere committed
59
60
61
62
63
            val factory = when (viewModelContext) {
                is FragmentViewModelContext -> viewModelContext.fragment as? Factory
                is ActivityViewModelContext -> viewModelContext.activity as? Factory
            }
            return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
64
65
66
67
68
69
70
        }
    }

    private val room = session.getRoom(initialState.roomId)!!

    init {
        observeRoomSummary()
71
        observeRoomHistoryVisibility()
72
73
        observeJoinRule()
        observeGuestAccess()
74
        observeRoomAvatar()
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
75
76
77
78
79
        observeState()
    }

    private fun observeState() {
        selectSubscribe(
80
                RoomSettingsViewState::avatarAction,
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
81
82
83
                RoomSettingsViewState::newName,
                RoomSettingsViewState::newTopic,
                RoomSettingsViewState::newHistoryVisibility,
84
                RoomSettingsViewState::newRoomJoinRules,
85
86
                RoomSettingsViewState::roomSummary) { avatarAction,
                                                      newName,
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
87
88
                                                      newTopic,
                                                      newHistoryVisibility,
89
                                                      newJoinRule,
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
90
91
92
93
                                                      asyncSummary ->
            val summary = asyncSummary()
            setState {
                copy(
94
95
                        showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None
                                || summary?.name != newName
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
96
                                || summary?.topic != newTopic
97
                                || (newHistoryVisibility != null && newHistoryVisibility != currentHistoryVisibility)
98
                                || newJoinRule.hasChanged()
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
99
100
101
                )
            }
        }
102
103
104
    }

    private fun observeRoomSummary() {
Valere's avatar
Valere committed
105
        room.rx().liveRoomSummary()
106
107
                .unwrap()
                .execute { async ->
108
109
110
                    val roomSummary = async.invoke()
                    copy(
                            roomSummary = async,
111
                            newName = roomSummary?.name,
112
                            newTopic = roomSummary?.topic
113
                    )
114
                }
115
116
117

        val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()

Benoît Marty's avatar
Cleanup    
Benoît Marty committed
118
119
120
121
        powerLevelsContentLive
                .subscribe {
                    val powerLevelsHelper = PowerLevelsHelper(it)
                    val permissions = RoomSettingsViewState.ActionPermissions(
122
                            canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR),
ganfra's avatar
ganfra committed
123
                            canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
124
                            canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
Benoît Marty's avatar
Benoît Marty committed
125
                            canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
126
127
128
129
                                    EventType.STATE_ROOM_HISTORY_VISIBILITY),
                            canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
                                    EventType.STATE_ROOM_JOIN_RULES)
                                    && powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
Valere's avatar
Valere committed
130
131
132
                                    EventType.STATE_ROOM_GUEST_ACCESS),
                            canAddChildren = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
                                    EventType.STATE_SPACE_CHILD)
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
133
134
135
136
                    )
                    setState { copy(actionPermissions = permissions) }
                }
                .disposeOnClear()
137
138
    }

139
140
141
142
143
144
    private fun observeRoomHistoryVisibility() {
        room.rx()
                .liveStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.NoCondition)
                .mapOptional { it.content.toModel<RoomHistoryVisibilityContent>() }
                .unwrap()
                .subscribe {
Valere's avatar
Valere committed
145
                    it.historyVisibility?.let {
146
147
148
149
150
151
                        setState { copy(currentHistoryVisibility = it) }
                    }
                }
                .disposeOnClear()
    }

Benoit Marty's avatar
Benoit Marty committed
152
    private fun observeJoinRule() {
153
154
155
156
157
158
159
160
161
162
163
164
        room.rx()
                .liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition)
                .mapOptional { it.content.toModel<RoomJoinRulesContent>() }
                .unwrap()
                .subscribe {
                    it.joinRules?.let {
                        setState { copy(currentRoomJoinRules = it) }
                    }
                }
                .disposeOnClear()
    }

Benoit Marty's avatar
Benoit Marty committed
165
    private fun observeGuestAccess() {
166
167
168
169
170
171
172
173
174
175
176
177
        room.rx()
                .liveStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.NoCondition)
                .mapOptional { it.content.toModel<RoomGuestAccessContent>() }
                .unwrap()
                .subscribe {
                    it.guestAccess?.let {
                        setState { copy(currentGuestAccess = it) }
                    }
                }
                .disposeOnClear()
    }

178
179
180
181
182
183
184
185
186
187
188
189
190
191
    /**
     * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
     */
    private fun observeRoomAvatar() {
        room.rx()
                .liveStateEvent(EventType.STATE_ROOM_AVATAR, QueryStringValue.NoCondition)
                .mapOptional { it.content.toModel<RoomAvatarContent>() }
                .unwrap()
                .subscribe {
                    setState { copy(currentRoomAvatarUrl = it.avatarUrl) }
                }
                .disposeOnClear()
    }

192
193
    override fun handle(action: RoomSettingsAction) {
        when (action) {
194
            is RoomSettingsAction.SetAvatarAction          -> handleSetAvatarAction(action)
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
195
196
197
            is RoomSettingsAction.SetRoomName              -> setState { copy(newName = action.newName) }
            is RoomSettingsAction.SetRoomTopic             -> setState { copy(newTopic = action.newTopic) }
            is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) }
198
            is RoomSettingsAction.SetRoomJoinRule          -> handleSetRoomJoinRule(action)
Valere's avatar
Valere committed
199
            is RoomSettingsAction.SetRoomGuestAccess       -> handleSetGuestAccess(action)
200
            is RoomSettingsAction.Save                     -> saveSettings()
201
            is RoomSettingsAction.Cancel                   -> cancel()
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
202
        }.exhaustive
203
204
    }

205
206
207
    private fun handleSetRoomJoinRule(action: RoomSettingsAction.SetRoomJoinRule) = withState { state ->
        setState {
            copy(newRoomJoinRules = RoomSettingsViewState.NewJoinRule(
Valere's avatar
Valere committed
208
209
210
211
212
213
214
215
216
217
218
                    newJoinRules = action.roomJoinRule.takeIf { it != state.currentRoomJoinRules },
                    newGuestAccess = state.newRoomJoinRules.newGuestAccess.takeIf { it != state.currentGuestAccess }
            ))
        }
    }

    private fun handleSetGuestAccess(action: RoomSettingsAction.SetRoomGuestAccess) = withState { state ->
        setState {
            copy(newRoomJoinRules = RoomSettingsViewState.NewJoinRule(
                    newJoinRules = state.newRoomJoinRules.newJoinRules.takeIf { it != state.currentRoomJoinRules },
                    newGuestAccess = action.guestAccess.takeIf { it != state.currentGuestAccess }
219
220
221
222
            ))
        }
    }

223
    private fun handleSetAvatarAction(action: RoomSettingsAction.SetAvatarAction) {
224
225
226
227
        setState {
            deletePendingAvatar(this)
            copy(avatarAction = action.avatarAction)
        }
228
229
    }

230
    private fun deletePendingAvatar(state: RoomSettingsViewState) {
231
        // Maybe delete the pending avatar
232
233
        (state.avatarAction as? RoomSettingsViewState.AvatarAction.UpdateAvatar)
                ?.let { tryOrNull { it.newAvatarUri.toFile().delete() } }
234
235
236
    }

    private fun cancel() {
237
        withState { deletePendingAvatar(it) }
238
239
240
241

        _viewEvents.post(RoomSettingsViewEvents.GoBack)
    }

242
243
244
245
246
247
248
    private fun saveSettings() = withState { state ->
        postLoading(true)

        val operationList = mutableListOf<Completable>()

        val summary = state.roomSummary.invoke()

249
        when (val avatarAction = state.avatarAction) {
Valere's avatar
Valere committed
250
251
            RoomSettingsViewState.AvatarAction.None -> Unit
            RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
252
253
254
255
256
257
                operationList.add(room.rx().deleteAvatar())
            }
            is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
                operationList.add(room.rx().updateAvatar(avatarAction.newAvatarUri, avatarAction.newAvatarFileName))
            }
        }
258
        if (summary?.name != state.newName) {
259
260
261
262
263
264
            operationList.add(room.rx().updateName(state.newName ?: ""))
        }
        if (summary?.topic != state.newTopic) {
            operationList.add(room.rx().updateTopic(state.newTopic ?: ""))
        }

265
        if (state.newHistoryVisibility != null) {
Benoît Marty's avatar
Cleanup    
Benoît Marty committed
266
            operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility))
267
268
        }

269
270
271
272
        if (state.newRoomJoinRules.hasChanged()) {
            operationList.add(room.rx().updateJoinRule(state.newRoomJoinRules.newJoinRules, state.newRoomJoinRules.newGuestAccess))
        }

273
274
        Observable
                .fromIterable(operationList)
275
                .concatMapCompletable { it }
276
277
278
                .subscribe(
                        {
                            postLoading(false)
279
280
                            setState {
                                deletePendingAvatar(this)
281
282
                                copy(
                                        avatarAction = RoomSettingsViewState.AvatarAction.None,
283
284
                                        newHistoryVisibility = null,
                                        newRoomJoinRules = RoomSettingsViewState.NewJoinRule()
285
                                )
286
                            }
287
288
289
290
291
292
293
                            _viewEvents.post(RoomSettingsViewEvents.Success)
                        },
                        {
                            postLoading(false)
                            _viewEvents.post(RoomSettingsViewEvents.Failure(it))
                        }
                )
294
295
    }

296
297
298
299
300
    private fun postLoading(isLoading: Boolean) {
        setState {
            copy(isLoading = isLoading)
        }
    }
301
}